fixed json overwrite

This commit is contained in:
emdee@spm.plastiras.org 2024-02-08 07:39:15 +00:00
parent e778108834
commit c70c501fdd
12 changed files with 283 additions and 25 deletions

View file

@ -38,6 +38,7 @@ written in pure Python3.
- Changing nospam - Changing nospam
- File resuming - File resuming
- Read receipts - Read receipts
- uses gevent
- NGC groups - NGC groups
### Screenshots ### Screenshots
@ -82,6 +83,7 @@ Weechat has a Jabber plugin to enable XMPP:
/help jabber /help jabber
``` ```
so you can have Tox, IRC and XMPP in the same application! so you can have Tox, IRC and XMPP in the same application!
See docs/ToxygenWeechat.md
## Install ## Install

View file

@ -55,8 +55,11 @@ line.
Migrate PyQt5 to qtpy - almost done. Migrate PyQt5 to qtpy - almost done.
Maybe migrate gevent to asyncio, or look at https://pypi.org/project/asyncio-gevent/ Maybe migrate gevent to asyncio, and migrate to
[qasync](https://github.com/CabbageDevelopment/qasync)
(see https://git.plastiras.org/emdee/phantompy ).
(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead).
## Standards ## Standards

153
docs/ToxygenWeechat.md Normal file
View file

@ -0,0 +1,153 @@
## Toxygen Weechat
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
```
/set relay.network.ipv6 off
/set relay.network.password password
/relay add weechat 9000
/relay start weechat
```
and use the Plugins/Weechat Console to start weechat under Toxygen.
Then use the 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!
### Creating servers for IRC over Tor
Create a proxy called tor
```
/proxy add tor socks5 127.0.0.1 9050
```
It should now show up in the list of proxies.
```
/proxy list
```
```
/nick SyniTox
```
## TLS certificates
[Create a Self-signed Certificate](https://www.oftc.net/NickServ/CertFP/)
Choose a SyniTox you will identify as.
Create a directory for your certificates ~/.config/weechat/ssl/
and make a subdirectory for each server ~/.config/weechat/ssl/irc.oftc.net/
Change to the server directory and use openssl to make a keypair and answer the questions:
```
openssl req -nodes -newkey rsa:2048 -keyout SyniTox.key -x509 -days 3650 -out SyniTox.cer
chmod 400 SyniTox.key
```
We now combine certificate and key to a single file SyniTox.pem
```
cat SyniTox.cer SyniTox.key > SyniTox.pem
chmod 400 SyniTox.pem
```
Do this for each server you want to connect to, or just use one for all of them.
### Libera TokTok channel
The main discussion forum for Tox is the #TokTok channel on libera.
libera has an onion server so we can map an address in tor. Add this
to your /etc/tor/torrc
```
MapAddress palladium.libera.chat libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion
```
Define the server in weechat
https://www.weechat.org/files/doc/stable/weechat_user.en.html#irc_sasl_authentication
```
/server remove libera
/server add libera palladium.libera.chat/6697 -tls -tls_verify
/set irc.server.libera.ipv6 off
/set irc.server.libera.proxy tor
/set irc.server.libera.username SyniTox
/set irc.server.libera.nicks SyniTox
/set irc.server.libera.tls on
/set irc.server.libera.tls_cert "${weechat_config_dir}/ssl/libera.chat/SyniTox.pem"
```
```
/set irc.server.libera.sasl_mechanism ecdsa-nist256p-challenge
/set irc.server.libera.sasl_username "SyniTox"
/set irc.server.libera.sasl_key "${weechat_config_dir}/ssl/libera.chat/SyniTox.pem"
```
Disconnect and connect back to the server.
```
/disconnect libera
/connect libera
```
/msg nickserv identify password SyniTox
### oftc.net
To use oftc.net over tor, you need to authenticate by SSL certificates.
Define the server in weechat
```
/server remove irc.oftc.net
/server add OFTC irc.oftc.net/6697 -tls -tls_verify
/set irc.server.OFTC.ipv6 off
/set irc.server.OFTC.proxy tor
/set irc.server.OFTC.username SyniTox
/set irc.server.OFTC.nicks SyniTox
/set irc.server.OFTC.tls on
/set irc.server.OFTC.tls_cert "${weechat_config_dir}/ssl/irc.oftc.chat/SyniTox.pem"
# Disconnect and connect back to the server.
/disconnect OFTC
/connect OFTC
```
You must be identified in order to validate using certs
```
/msg nickserv identify password SyniTox
```
To allow NickServ to identify you based on this certificate you need
to associate the certificate fingerprint with your nick. To do this
issue the command cert add to Nickserv (try /msg nickserv helpcert).
```
/msg nickserv cert add
```
### Privacy
[Add somes settings bellow to weechat](https://szorfein.github.io/weechat/tor/configure-weechat/).
Detail from [faq](https://weechat.org/files/doc/weechat_faq.en.html#security).
```
/set irc.server_default.msg_part ""
/set irc.server_default.msg_quit ""
/set irc.ctcp.clientinfo ""
/set irc.ctcp.finger ""
/set irc.ctcp.source ""
/set irc.ctcp.time ""
/set irc.ctcp.userinfo ""
/set irc.ctcp.version ""
/set irc.ctcp.ping ""
/plugin unload xfer
/set weechat.plugin.autoload "*,!xfer"
```

67
docs/todo.md Normal file
View file

@ -0,0 +1,67 @@
# Toxygen ToDo List
## Bugs
1. There is an agravating bug where new messages are not put in the
current window, and a messages waiting indicator appears. You have
to focus out of the window and then back in the window. this may be
fixed already
2. The tray icon is flaky and has been disabled - look in app.py
for bSHOW_TRAY
## Fix history
## Fix Audio
The code is in there but it's not working. It looks like audio input
is working but not output. The code is all in there; I may have broken
it trying to wire up the ability to set the audio device from the
command line.
## Fix Video
The code is in there but it's not working. I may have broken it
trying to wire up the ability to set the video device from the command
line.
## NGC Groups
1. peer_id There has been a change of API on a field named
```group.peer_id``` The code is broken in places because I have not
seen the path to change from the old API ro the new one.
## Plugin system
1. Needs better documentation and checking.
2. There's something broken in the way some of them plug into Qt menus.
3. Should the plugins be in toxygen or a separate repo?
4. There needs to be a uniform way for plugins to wire into callbacks.
## check toxygen_wrapper
1. I've broken out toxygen_wrapper to be standalone,
https://git.plastiras.org/emdee/toxygen_wrapper but the tox.py
needs each call double checking.
2. https://git.plastiras.org/emdee/toxygen_wrapper needs packaging
and making a dependency.
## Migration
Migrate PyQt5 to qtpy - almost done.
Maybe migrate gevent to asyncio, and migrate to
[qasync](https://github.com/CabbageDevelopment/qasync)
(see https://git.plastiras.org/emdee/phantompy ).
(Also look at https://pypi.org/project/asyncio-gevent/ but it's dead).
## Standards
There's a standard for Tox clients that this has not been tested against:
https://tox.gitbooks.io/tox-client-standard/content/general_requirements/general_requirements.html

View file

@ -6,6 +6,8 @@ import logging
import signal import signal
import time import time
from gevent import monkey; monkey.patch_all(); del monkey # noqa
import faulthandler import faulthandler
faulthandler.enable() faulthandler.enable()

View file

@ -7,7 +7,6 @@ import threading
from time import sleep, time from time import sleep, time
from copy import deepcopy from copy import deepcopy
from gevent import monkey; monkey.patch_all(); del monkey # noqa
import gevent import gevent
from qtpy import QtWidgets, QtGui, QtCore from qtpy import QtWidgets, QtGui, QtCore
@ -182,6 +181,7 @@ class App:
if uri is not None and uri.startswith('tox:'): if uri is not None and uri.startswith('tox:'):
self._uri = uri[4:] self._uri = uri[4:]
self._history = None self._history = None
self.bAppExiting = False
# Public methods # Public methods
@ -296,7 +296,7 @@ class App:
def _stop_app(self) -> None: def _stop_app(self) -> None:
LOG.debug("_stop_app") LOG.debug("_stop_app")
self._save_profile() self._save_profile()
#? self._history.save_history() self._history.save_history()
self._plugin_loader.stop() self._plugin_loader.stop()
try: try:
@ -304,10 +304,16 @@ class App:
except (Exception, RuntimeError): except (Exception, RuntimeError):
# RuntimeError: cannot join current thread # RuntimeError: cannot join current thread
pass pass
# I think there are threads still running here leading to a SEGV
# File "/usr/lib/python3.11/threading.py", line 1401 in run
# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner
# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap
if hasattr(self, '_tray') and self._tray: if hasattr(self, '_tray') and self._tray:
self._tray.hide() self._tray.hide()
self._settings.close() self._settings.close()
self.bAppExiting = True
LOG.debug(f"stop_app: Killing {self._tox}") LOG.debug(f"stop_app: Killing {self._tox}")
self._kill_toxav() self._kill_toxav()
self._kill_tox() self._kill_tox()
@ -522,18 +528,21 @@ class App:
return ls.result return ls.result
def _load_existing_profile(self, profile_path) -> None: def _load_existing_profile(self, profile_path) -> None:
profile_path = profile_path.replace('.json', '.tox')
LOG.info("_load_existing_profile " +repr(profile_path)) LOG.info("_load_existing_profile " +repr(profile_path))
assert os.path.exists(profile_path), profile_path assert os.path.exists(profile_path), profile_path
self._profile_manager = ProfileManager(self._toxes, profile_path) self._profile_manager = ProfileManager(self._toxes, profile_path, app=self)
data = self._profile_manager.open_profile() data = self._profile_manager.open_profile()
if self._toxes.is_data_encrypted(data): if self._toxes.is_data_encrypted(data):
LOG.debug("_entering password") LOG.debug("_entering password")
data = self._enter_password(data) data = self._enter_password(data)
LOG.debug("_entered password") LOG.debug("_entered password")
json_file = profile_path.replace('.tox', '.json') json_file = profile_path.replace('.tox', '.json')
assert os.path.exists(json_file), json_file if os.path.exists(json_file):
LOG.debug("creating _settings from: " +json_file) LOG.debug("creating _settings from: " +json_file)
self._settings = Settings(self._toxes, json_file, self) self._settings = Settings(self._toxes, json_file, self)
else:
self._tox = self._create_tox(data, self._settings) self._tox = self._create_tox(data, self._settings)
LOG.debug("created _tox") LOG.debug("created _tox")
@ -683,7 +692,8 @@ class App:
self._contacts_provider = ContactProvider(self._tox, self._contacts_provider = ContactProvider(self._tox,
self._friend_factory, self._friend_factory,
self._group_factory, self._group_factory,
self._group_peer_factory) self._group_peer_factory,
app=self)
self._profile = Profile(self._profile_manager, self._profile = Profile(self._profile_manager,
self._tox, self._tox,
self._ms, self._ms,

View file

@ -11,12 +11,13 @@ from av.calls import LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE
class ContactProvider(tox_save.ToxSave): class ContactProvider(tox_save.ToxSave):
def __init__(self, tox, friend_factory, group_factory, group_peer_factory): def __init__(self, tox, friend_factory, group_factory, group_peer_factory, app=None):
super().__init__(tox) super().__init__(tox)
self._friend_factory = friend_factory self._friend_factory = friend_factory
self._group_factory = group_factory self._group_factory = group_factory
self._group_peer_factory = group_peer_factory self._group_peer_factory = group_peer_factory
self._cache = {} # key - contact's public key, value - contact instance self._cache = {} # key - contact's public key, value - contact instance
self._app = app
# Friends # Friends
@ -41,6 +42,8 @@ class ContactProvider(tox_save.ToxSave):
return friend return friend
def get_all_friends(self): def get_all_friends(self):
if self._app and self._app.bAppExiting:
return
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:

View file

@ -6,6 +6,7 @@ import common.tox_save as tox_save
from middleware.threads import invoke_in_main_thread from middleware.threads import invoke_in_main_thread
iUMAXINT = 4294967295 iUMAXINT = 4294967295
iRECONNECT = 50
global LOG global LOG
import logging import logging
@ -15,7 +16,7 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave):
""" """
Profile of current toxygen user. Profile of current toxygen user.
""" """
def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action): def __init__(self, profile_manager, tox, screen, contacts_provider, reset_action, app=None):
""" """
:param tox: tox instance :param tox: tox instance
:param screen: ref to main screen :param screen: ref to main screen
@ -34,32 +35,33 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave):
self._reset_action = reset_action self._reset_action = reset_action
self._waiting_for_reconnection = False self._waiting_for_reconnection = False
self._timer = None self._timer = None
self._app = app
# Edit current user's data # Edit current user's data
def change_status(self): def change_status(self) -> None:
""" """
Changes status of user (online, away, busy) Changes status of user (online, away, busy)
""" """
if self._status is not None: if self._status is not None:
self.set_status((self._status + 1) % 3) self.set_status((self._status + 1) % 3)
def set_status(self, status): def set_status(self, status) -> None:
super().set_status(status) super().set_status(status)
if status is not None: if status is not None:
self._tox.self_set_status(status) self._tox.self_set_status(status)
elif not self._waiting_for_reconnection: elif not self._waiting_for_reconnection:
self._waiting_for_reconnection = True self._waiting_for_reconnection = True
self._timer = threading.Timer(50, self._reconnect) self._timer = threading.Timer(iRECONNECT, self._reconnect)
self._timer.start() self._timer.start()
def set_name(self, value): def set_name(self, value) -> None:
if self.name == value: if self.name == value:
return return
super().set_name(value) super().set_name(value)
self._tox.self_set_name(self._name) self._tox.self_set_name(self._name)
def set_status_message(self, value): def set_status_message(self, value) -> None:
super().set_status_message(value) super().set_status_message(value)
self._tox.self_set_status_message(self._status_message) self._tox.self_set_status_message(self._status_message)
@ -72,19 +74,34 @@ class Profile(basecontact.BaseContact, tox_save.ToxSave):
# Reset # Reset
def restart(self): def restart(self) -> None:
""" """
Recreate tox instance Recreate tox instance
""" """
self.status = None self.status = None
invoke_in_main_thread(self._reset_action) invoke_in_main_thread(self._reset_action)
def _reconnect(self): def _reconnect(self) -> None:
self._waiting_for_reconnection = False self._waiting_for_reconnection = False
if self._app and self._app.bAppExiting:
# dont do anything after the app has been shipped
# there's a segv that results
return
contacts = self._contacts_provider.get_all_friends() contacts = self._contacts_provider.get_all_friends()
all_friends_offline = all(list(map(lambda x: x.status is None, contacts))) all_friends_offline = all(list(map(lambda x: x.status is None, contacts)))
if self.status is None or (all_friends_offline and len(contacts)): if self.status is None or (all_friends_offline and len(contacts)):
self._waiting_for_reconnection = True self._waiting_for_reconnection = True
self.restart() self.restart()
self._timer = threading.Timer(50, self._reconnect) self._timer = threading.Timer(iRECONNECT, self._reconnect)
self._timer.start() self._timer.start()
# Current thread 0x00007901a13ccb80 (most recent call first):
# File "/usr/local/lib/python3.11/site-packages/tox_wrapper/tox.py", line 826 in self_get_friend_list_size
# File "/usr/local/lib/python3.11/site-packages/tox_wrapper/tox.py", line 838 in self_get_friend_list
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/contact_provider.py", line 45 in get_all_friends
# File "/mnt/o/var/local/src/toxygen/toxygen/contacts/profile.py", line 90 in _reconnect
# File "/usr/lib/python3.11/threading.py", line 1401 in run
# File "/usr/lib/python3.11/threading.py", line 1045 in _bootstrap_inner
# File "/usr/lib/python3.11/threading.py", line 1002 in _bootstrap
#

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,15 @@
# -*- 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 qtpy import QtCore, QtGui, QtWidgets, uic from qtpy import QtCore, QtGui, QtWidgets, uic
import tox_wrapper.tests.support_testing as ts
with ts.ignoreStderr(): # not out
import pyaudio
from user_data.settings import * from user_data.settings import *
from utils.util import * from utils.util import *
from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow from ui.widgets import CenteredWidget, DataLabel, LineEdit, RubberBandWindow
import updater.updater as updater import updater.updater as updater
import utils.ui as util_ui import utils.ui as util_ui
import tox_wrapper.tests.support_testing as ts
with ts.ignoreStderr():
import pyaudio
from user_data import settings from user_data import settings
global LOG global LOG

View file

@ -15,11 +15,12 @@ class ProfileManager:
""" """
Class with methods for search, load and save profiles Class with methods for search, load and save profiles
""" """
def __init__(self, toxes, path): def __init__(self, toxes, path, app=None):
assert path assert path
self._toxes = toxes self._toxes = toxes
self._path = path self._path = path
assert path assert path
self._app = app
self._directory = os.path.dirname(path) self._directory = os.path.dirname(path)
self._profile_saved_event = Event() self._profile_saved_event = Event()
# create /avatars if not exists: # create /avatars if not exists:

View file

@ -138,7 +138,7 @@ class Settings(dict):
self._args = app._args self._args = app._args
self._oArgs = 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._profile_path = app._path # json_path.replace('.json', '.tox') self._profile_path = json_path.replace('.json', '.tox')
self._settings_saved_event = Event() self._settings_saved_event = Event()
path = json_path.replace('.tox', '.json') path = json_path.replace('.tox', '.json')