diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..44f99e0
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "tox_wrapper"
+version = "1.0.0"
+description = "A Python3 ctypes wrapping of c-toxcore into Python."
+authors = ["Ingvar"]
+repository = "https://git.plastiras.org/emdee/toxygen_wrapper"
+keywords = ["tox","python3","ctypes"]
+license = "GPL3"
+
+[tool.poetry.dependencies]
+python = ">=3.6;<3.12"
+
+# Stuff needed for development, but not for install&usage
+# [tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["setuptools"]
+
diff --git a/tox_wrapper/.#toxencryptsave.py b/tox_wrapper/.#toxencryptsave.py
new file mode 120000
index 0000000..c7d1b0b
--- /dev/null
+++ b/tox_wrapper/.#toxencryptsave.py
@@ -0,0 +1 @@
+root@devuan.29604:1700913818
\ No newline at end of file
diff --git a/tox_wrapper/__init__.py b/tox_wrapper/__init__.py
new file mode 100644
index 0000000..2b8d997
--- /dev/null
+++ b/tox_wrapper/__init__.py
@@ -0,0 +1,5 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+# You need a libs directory beside this directory
+# and you need to link your libtoxcore.so and libtoxav.so
+# and libtoxencryptsave.so into ../libs/
+# Link all 3 to libtoxcore.so if you have only libtoxcore.so
diff --git a/tox_wrapper/libtox.py b/tox_wrapper/libtox.py
new file mode 100644
index 0000000..bf39dd0
--- /dev/null
+++ b/tox_wrapper/libtox.py
@@ -0,0 +1,87 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+import os
+import sys
+from ctypes import CDLL
+
+# You need a libs directory beside this directory
+# and you need to link your libtoxcore.so and libtoxav.so
+# and libtoxencryptsave.so into ../libs/
+# Link all 3 to libtoxcore.so if you have only libtoxcore.so
+try:
+ import utils.util as util
+ sLIBS_DIR = util.get_libs_directory()
+except ImportError:
+ sLIBS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
+ 'libs')
+
+# environment variable TOXCORE_LIBS overrides
+d = os.environ.get('TOXCORE_LIBS', '')
+if d and os.path.exists(d):
+ sLIBS_DIR = d
+ if os.environ.get('DEBUG', ''):
+ print ('DBUG: Setting TOXCORE_LIBS to ' +d)
+del d
+
+class LibToxCore:
+
+ def __init__(self):
+ platform = sys.platform
+ if platform == 'win32':
+ libtoxcore = 'libtox.dll'
+ elif platform == 'darwin':
+ libtoxcore = 'libtoxcore.dylib'
+ else:
+ libtoxcore = 'libtoxcore.so'
+
+ # libtoxcore and libsodium may be installed in your os
+ # give libs/ precedence
+ libFile = os.path.join(sLIBS_DIR, libtoxcore)
+ if os.path.isfile(libFile):
+ self._libtoxcore = CDLL(libFile)
+ else:
+ self._libtoxcore = CDLL(libtoxcore)
+
+ def __getattr__(self, item):
+ return self._libtoxcore.__getattr__(item)
+
+class LibToxAV:
+
+ def __init__(self):
+ platform = sys.platform
+ if platform == 'win32':
+ # on Windows av api is in libtox.dll
+ self._libtoxav = CDLL(os.path.join(sLIBS_DIR, 'libtox.dll'))
+ elif platform == 'darwin':
+ self._libtoxav = CDLL('libtoxcore.dylib')
+ else:
+ libFile = os.path.join(sLIBS_DIR, 'libtoxav.so')
+ if os.path.isfile(libFile):
+ self._libtoxav = CDLL(libFile)
+ else:
+ self._libtoxav = CDLL('libtoxav.so')
+
+ def __getattr__(self, item):
+ return self._libtoxav.__getattr__(item)
+
+# figure out how to see if we have a combined library
+
+class LibToxEncryptSave:
+
+ def __init__(self):
+ platform = sys.platform
+ if platform == 'win32':
+ # on Windows profile encryption api is in libtox.dll
+ self._lib_tox_encrypt_save = CDLL(os.path.join(sLIBS_DIR, 'libtox.dll'))
+ elif platform == 'darwin':
+ self._lib_tox_encrypt_save = CDLL('libtoxcore.dylib')
+ else:
+ libFile = os.path.join(sLIBS_DIR, 'libtoxencryptsave.so')
+ if os.path.isfile(libFile):
+ self._lib_tox_encrypt_save = CDLL(libFile)
+ else:
+ self._lib_tox_encrypt_save = CDLL('libtoxencryptsave.so')
+
+ def __getattr__(self, item):
+ return self._lib_tox_encrypt_save.__getattr__(item)
+
+# figure out how to see if we have a combined library
diff --git a/tox_wrapper/tests/.#support_onions.py b/tox_wrapper/tests/.#support_onions.py
new file mode 120000
index 0000000..c7d1b0b
--- /dev/null
+++ b/tox_wrapper/tests/.#support_onions.py
@@ -0,0 +1 @@
+root@devuan.29604:1700913818
\ No newline at end of file
diff --git a/tox_wrapper/tests/__init__.py b/tox_wrapper/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tox_wrapper/tests/socks.py b/tox_wrapper/tests/socks.py
new file mode 100644
index 0000000..fa1b25e
--- /dev/null
+++ b/tox_wrapper/tests/socks.py
@@ -0,0 +1,391 @@
+"""SocksiPy - Python SOCKS module.
+Version 1.00
+
+Copyright 2006 Dan-Haim. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+3. Neither the name of Dan Haim nor the names of his contributors may be used
+ to endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
+
+
+This module provides a standard socket-like interface for Python
+for tunneling connections through SOCKS proxies.
+
+"""
+
+"""
+
+Minor modifications made by Christopher Gilbert (http://motomastyle.com/)
+for use in PyLoris (http://pyloris.sourceforge.net/)
+
+Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
+mainly to merge bug fixes found in Sourceforge
+
+Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/)
+
+"""
+
+import socket
+import struct
+import sys
+
+PROXY_TYPE_SOCKS4 = 1
+PROXY_TYPE_SOCKS5 = 2
+PROXY_TYPE_HTTP = 3
+
+_defaultproxy = None
+_orgsocket = socket.socket
+
+class ProxyError(Exception): pass
+class GeneralProxyError(ProxyError): pass
+class Socks5AuthError(ProxyError): pass
+class Socks5Error(ProxyError): pass
+class Socks4Error(ProxyError): pass
+class HTTPError(ProxyError): pass
+
+_generalerrors = ("success",
+ "invalid data",
+ "not connected",
+ "not available",
+ "bad proxy type",
+ "bad input")
+
+_socks5errors = ("succeeded",
+ "general SOCKS server failure",
+ "connection not allowed by ruleset",
+ "Network unreachable",
+ "Host unreachable",
+ "Connection refused",
+ "TTL expired",
+ "Command not supported",
+ "Address type not supported",
+ "Unknown error")
+
+_socks5autherrors = ("succeeded",
+ "authentication is required",
+ "all offered authentication methods were rejected",
+ "unknown username or invalid password",
+ "unknown error")
+
+_socks4errors = ("request granted",
+ "request rejected or failed",
+ "request rejected because SOCKS server cannot connect to identd on the client",
+ "request rejected because the client program and identd report different user-ids",
+ "unknown error")
+
+def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None) -> None:
+ """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
+ Sets a default proxy which all further socksocket objects will use,
+ unless explicitly changed.
+ """
+ global _defaultproxy
+ _defaultproxy = (proxytype, addr, port, rdns, username, password)
+
+def wrapmodule(module) -> None:
+ """wrapmodule(module)
+ Attempts to replace a module's socket library with a SOCKS socket. Must set
+ a default proxy using setdefaultproxy(...) first.
+ This will only work on modules that import socket directly into the namespace;
+ most of the Python Standard Library falls into this category.
+ """
+ if _defaultproxy != None:
+ module.socket.socket = socksocket
+ else:
+ raise GeneralProxyError((4, "no proxy specified"))
+
+class socksocket(socket.socket):
+ """socksocket([family[, type[, proto]]]) -> socket object
+ Open a SOCKS enabled socket. The parameters are the same as
+ those of the standard socket init. In order for SOCKS to work,
+ you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
+ """
+
+ def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
+ _orgsocket.__init__(self, family, type, proto, _sock)
+ if _defaultproxy != None:
+ self.__proxy = _defaultproxy
+ else:
+ self.__proxy = (None, None, None, None, None, None)
+ self.__proxysockname = None
+ self.__proxypeername = None
+
+ def __recvall(self, count):
+ """__recvall(count) -> data
+ Receive EXACTLY the number of bytes requested from the socket.
+ Blocks until the required number of bytes have been received.
+ """
+ data = self.recv(count)
+ while len(data) < count:
+ d = self.recv(count-len(data))
+ if not d: raise GeneralProxyError((0, "connection closed unexpectedly"))
+ data = data + d
+ return data
+
+ def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
+ """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
+ Sets the proxy to be used.
+ proxytype - The type of the proxy to be used. Three types
+ are supported: PROXY_TYPE_SOCKS4 (including socks4a),
+ PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
+ addr - The address of the server (IP or DNS).
+ port - The port of the server. Defaults to 1080 for SOCKS
+ servers and 8080 for HTTP proxy servers.
+ rdns - Should DNS queries be preformed on the remote side
+ (rather than the local side). The default is True.
+ Note: This has no effect with SOCKS4 servers.
+ username - Username to authenticate with to the server.
+ The default is no authentication.
+ password - Password to authenticate with to the server.
+ Only relevant when username is also provided.
+ """
+ self.__proxy = (proxytype, addr, port, rdns, username, password)
+
+ def __negotiatesocks5(self, destaddr, destport):
+ """__negotiatesocks5(self,destaddr,destport)
+ Negotiates a connection through a SOCKS5 server.
+ """
+ # First we'll send the authentication packages we support.
+ if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
+ # The username/password details were supplied to the
+ # setproxy method so we support the USERNAME/PASSWORD
+ # authentication (in addition to the standard none).
+ self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
+ else:
+ # No username/password were entered, therefore we
+ # only support connections with no authentication.
+ self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00))
+ # We'll receive the server's response to determine which
+ # method was selected
+ chosenauth = self.__recvall(2)
+ if chosenauth[0:1] != chr(0x05).encode():
+ self.close()
+ raise GeneralProxyError((1, _generalerrors[1]))
+ # Check the chosen authentication method
+ if chosenauth[1:2] == chr(0x00).encode():
+ # No authentication is required
+ pass
+ elif chosenauth[1:2] == chr(0x02).encode():
+ # Okay, we need to perform a basic username/password
+ # authentication.
+ self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5])
+ authstat = self.__recvall(2)
+ if authstat[0:1] != chr(0x01).encode():
+ # Bad response
+ self.close()
+ raise GeneralProxyError((1, _generalerrors[1]))
+ if authstat[1:2] != chr(0x00).encode():
+ # Authentication failed
+ self.close()
+ raise Socks5AuthError((3, _socks5autherrors[3]))
+ # Authentication succeeded
+ else:
+ # Reaching here is always bad
+ self.close()
+ if chosenauth[1] == chr(0xFF).encode():
+ raise Socks5AuthError((2, _socks5autherrors[2]))
+ else:
+ raise GeneralProxyError((1, _generalerrors[1]))
+ # Now we can request the actual connection
+ req = struct.pack('BBB', 0x05, 0x01, 0x00)
+ # If the given destination address is an IP address, we'll
+ # use the IPv4 address request even if remote resolving was specified.
+ try:
+ ipaddr = socket.inet_aton(destaddr)
+ req = req + chr(0x01).encode() + ipaddr
+ except socket.error:
+ # Well it's not an IP number, so it's probably a DNS name.
+ if self.__proxy[3]:
+ # Resolve remotely
+ ipaddr = None
+ if type(destaddr) != type(b''): # python3
+ destaddr_bytes = destaddr.encode(encoding='idna')
+ else:
+ destaddr_bytes = destaddr
+ req = req + chr(0x03).encode() + chr(len(destaddr_bytes)).encode() + destaddr_bytes
+ else:
+ # Resolve locally
+ ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
+ req = req + chr(0x01).encode() + ipaddr
+ req = req + struct.pack(">H", destport)
+ self.sendall(req)
+ # Get the response
+ resp = self.__recvall(4)
+ if resp[0:1] != chr(0x05).encode():
+ self.close()
+ raise GeneralProxyError((1, _generalerrors[1]))
+ elif resp[1:2] != chr(0x00).encode():
+ # Connection failed
+ self.close()
+ if ord(resp[1:2])<=8:
+ raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
+ else:
+ raise Socks5Error((9, _socks5errors[9]))
+ # Get the bound address/port
+ elif resp[3:4] == chr(0x01).encode():
+ boundaddr = self.__recvall(4)
+ elif resp[3:4] == chr(0x03).encode():
+ resp = resp + self.recv(1)
+ boundaddr = self.__recvall(ord(resp[4:5]))
+ else:
+ self.close()
+ raise GeneralProxyError((1,_generalerrors[1]))
+ boundport = struct.unpack(">H", self.__recvall(2))[0]
+ self.__proxysockname = (boundaddr, boundport)
+ if ipaddr != None:
+ self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
+ else:
+ self.__proxypeername = (destaddr, destport)
+
+ def getproxysockname(self):
+ """getsockname() -> address info
+ Returns the bound IP address and port number at the proxy.
+ """
+ return self.__proxysockname
+
+ def getproxypeername(self):
+ """getproxypeername() -> address info
+ Returns the IP and port number of the proxy.
+ """
+ return _orgsocket.getpeername(self)
+
+ def getpeername(self):
+ """getpeername() -> address info
+ Returns the IP address and port number of the destination
+ machine (note: getproxypeername returns the proxy)
+ """
+ return self.__proxypeername
+
+ def __negotiatesocks4(self,destaddr,destport) -> None:
+ """__negotiatesocks4(self,destaddr,destport)
+ Negotiates a connection through a SOCKS4 server.
+ """
+ # Check if the destination address provided is an IP address
+ rmtrslv = False
+ try:
+ ipaddr = socket.inet_aton(destaddr)
+ except socket.error:
+ # It's a DNS name. Check where it should be resolved.
+ if self.__proxy[3]:
+ ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
+ rmtrslv = True
+ else:
+ ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
+ # Construct the request packet
+ req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
+ # The username parameter is considered userid for SOCKS4
+ if self.__proxy[4] != None:
+ req = req + self.__proxy[4]
+ req = req + chr(0x00).encode()
+ # DNS name if remote resolving is required
+ # NOTE: This is actually an extension to the SOCKS4 protocol
+ # called SOCKS4A and may not be supported in all cases.
+ if rmtrslv:
+ req = req + destaddr + chr(0x00).encode()
+ self.sendall(req)
+ # Get the response from the server
+ resp = self.__recvall(8)
+ if resp[0:1] != chr(0x00).encode():
+ # Bad data
+ self.close()
+ raise GeneralProxyError((1,_generalerrors[1]))
+ if resp[1:2] != chr(0x5A).encode():
+ # Server returned an error
+ self.close()
+ if ord(resp[1:2]) in (91, 92, 93):
+ self.close()
+ raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
+ else:
+ raise Socks4Error((94, _socks4errors[4]))
+ # Get the bound address/port
+ self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0])
+ if rmtrslv != None:
+ self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
+ else:
+ self.__proxypeername = (destaddr, destport)
+
+ def __negotiatehttp(self, destaddr, destport) -> None:
+ """__negotiatehttp(self,destaddr,destport)
+ Negotiates a connection through an HTTP server.
+ """
+ # If we need to resolve locally, we do this now
+ if not self.__proxy[3]:
+ addr = socket.gethostbyname(destaddr)
+ else:
+ addr = destaddr
+ self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode())
+ # We read the response until we get the string "\r\n\r\n"
+ resp = self.recv(1)
+ while resp.find("\r\n\r\n".encode()) == -1:
+ recv = self.recv(1)
+ if not recv:
+ raise GeneralProxyError((1, _generalerrors[1]))
+ resp = resp + recv
+ # We just need the first line to check if the connection
+ # was successful
+ statusline = resp.splitlines()[0].split(" ".encode(), 2)
+ if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
+ self.close()
+ raise GeneralProxyError((1, _generalerrors[1]))
+ try:
+ statuscode = int(statusline[1])
+ except ValueError:
+ self.close()
+ raise GeneralProxyError((1, _generalerrors[1]))
+ if statuscode != 200:
+ self.close()
+ raise HTTPError((statuscode, statusline[2]))
+ self.__proxysockname = ("0.0.0.0", 0)
+ self.__proxypeername = (addr, destport)
+
+ def connect(self, destpair) -> None:
+ """connect(self, despair)
+ Connects to the specified destination through a proxy.
+ destpar - A tuple of the IP/DNS address and the port number.
+ (identical to socket's connect).
+ To select the proxy server use setproxy().
+ """
+ # Do a minimal input check first
+ if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int):
+ raise GeneralProxyError((5, _generalerrors[5]))
+ if self.__proxy[0] == PROXY_TYPE_SOCKS5:
+ if self.__proxy[2] != None:
+ portnum = int(self.__proxy[2])
+ else:
+ portnum = 1080
+ _orgsocket.connect(self, (self.__proxy[1], portnum))
+ self.__negotiatesocks5(destpair[0], destpair[1])
+ elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
+ if self.__proxy[2] != None:
+ portnum = self.__proxy[2]
+ else:
+ portnum = 1080
+ _orgsocket.connect(self,(self.__proxy[1], portnum))
+ self.__negotiatesocks4(destpair[0], destpair[1])
+ elif self.__proxy[0] == PROXY_TYPE_HTTP:
+ if self.__proxy[2] != None:
+ portnum = self.__proxy[2]
+ else:
+ portnum = 8080
+ _orgsocket.connect(self,(self.__proxy[1], portnum))
+ self.__negotiatehttp(destpair[0], destpair[1])
+ elif self.__proxy[0] == None:
+ _orgsocket.connect(self, (destpair[0], destpair[1]))
+ else:
+ raise GeneralProxyError((4, _generalerrors[4]))
diff --git a/tox_wrapper/tests/support_http.py b/tox_wrapper/tests/support_http.py
new file mode 100644
index 0000000..f3bc975
--- /dev/null
+++ b/tox_wrapper/tests/support_http.py
@@ -0,0 +1,163 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+import os
+import sys
+import logging
+from io import BytesIO
+import urllib
+import traceback
+
+global LOG
+LOG = logging.getLogger('app.'+'ts')
+
+try:
+ import pycurl
+except ImportError:
+ pycurl = None
+try:
+ import requests
+except ImportError:
+ requests = None
+
+lNO_PROXY = ['localhost', '127.0.0.1']
+CONNECT_TIMEOUT = 20.0
+
+def bAreWeConnected() -> bool:
+ # FixMe: Linux only
+ sFile = f"/proc/{os.getpid()}/net/route"
+ if not os.path.isfile(sFile): return None
+ i = 0
+ for elt in open(sFile, "r").readlines():
+ if elt.startswith('Iface'): continue
+ if elt.startswith('lo'): continue
+ i += 1
+ return i > 0
+
+def pick_up_proxy_from_environ() -> dict:
+ retval = dict()
+ if os.environ.get('socks_proxy', ''):
+ # socks_proxy takes precedence over https/http
+ proxy = os.environ.get('socks_proxy', '')
+ i = proxy.find('//')
+ if i >= 0: proxy = proxy[i+2:]
+ retval['proxy_host'] = proxy.split(':')[0]
+ retval['proxy_port'] = proxy.split(':')[-1]
+ retval['proxy_type'] = 2
+ retval['udp_enabled'] = False
+ elif os.environ.get('https_proxy', ''):
+ # https takes precedence over http
+ proxy = os.environ.get('https_proxy', '')
+ i = proxy.find('//')
+ if i >= 0: proxy = proxy[i+2:]
+ retval['proxy_host'] = proxy.split(':')[0]
+ retval['proxy_port'] = proxy.split(':')[-1]
+ retval['proxy_type'] = 1
+ retval['udp_enabled'] = False
+ elif os.environ.get('http_proxy', ''):
+ proxy = os.environ.get('http_proxy', '')
+ i = proxy.find('//')
+ if i >= 0: proxy = proxy[i+2:]
+ retval['proxy_host'] = proxy.split(':')[0]
+ retval['proxy_port'] = proxy.split(':')[-1]
+ retval['proxy_type'] = 1
+ retval['udp_enabled'] = False
+ else:
+ retval['proxy_host'] = ''
+ retval['proxy_port'] = ''
+ retval['proxy_type'] = 0
+ retval['udp_enabled'] = True
+ return retval
+
+def download_url(url:str, settings:str = None) -> None:
+ if not bAreWeConnected(): return ''
+
+ if settings is None:
+ settings = pick_up_proxy_from_environ()
+
+ if pycurl:
+ LOG.debug('Downloading with pycurl: ' + str(url))
+ buffer = BytesIO()
+ c = pycurl.Curl()
+ c.setopt(c.URL, url)
+ c.setopt(c.WRITEDATA, buffer)
+ # Follow redirect.
+ c.setopt(c.FOLLOWLOCATION, True)
+
+ # cookie jar
+ cjar = os.path.join(os.environ['HOME'], '.local', 'jar.cookie')
+ if os.path.isfile(cjar):
+ c.setopt(c.COOKIEFILE, cjar)
+ # LARGS+=( --cookie-jar --junk-session-cookies )
+
+ #? c.setopt(c.ALTSVC_CTRL, 16)
+
+ c.setopt(c.NOPROXY, ','.join(lNO_PROXY))
+ #? c.setopt(c.CAINFO, certifi.where())
+ if settings['proxy_type'] == 2 and settings['proxy_host']:
+ socks_proxy = 'socks5h://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ settings['udp_enabled'] = False
+ c.setopt(c.PROXY, socks_proxy)
+ c.setopt(c.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5_HOSTNAME)
+ elif settings['proxy_type'] == 1 and settings['proxy_host']:
+ https_proxy = 'https://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ c.setopt(c.PROXY, https_proxy)
+ elif settings['proxy_type'] == 1 and settings['proxy_host']:
+ http_proxy = 'http://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ c.setopt(c.PROXY, http_proxy)
+ c.setopt(c.PROTOCOLS, c.PROTO_HTTPS)
+ try:
+ c.perform()
+ c.close()
+ #? assert c.getinfo(c.RESPONSE_CODE) < 300
+ result = buffer.getvalue()
+ # Body is a byte string.
+ LOG.info('nodes loaded with pycurl: ' + str(url))
+ return result
+ except Exception as ex:
+ LOG.error('TOX Downloading error with pycurl: ' + str(ex))
+ LOG.error('\n' + traceback.format_exc())
+ # drop through
+
+ if requests:
+ LOG.debug('Downloading with requests: ' + str(url))
+ try:
+ headers = dict()
+ headers['Content-Type'] = 'application/json'
+ proxies = dict()
+ if settings['proxy_type'] == 2 and settings['proxy_host']:
+ socks_proxy = 'socks5://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ settings['udp_enabled'] = False
+ proxies['https'] = socks_proxy
+ elif settings['proxy_type'] == 1 and settings['proxy_host']:
+ https_proxy = 'https://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ proxies['https'] = https_proxy
+ elif settings['proxy_type'] == 1 and settings['proxy_host']:
+ http_proxy = 'http://'+settings['proxy_host']+':'+str(settings['proxy_port'])
+ proxies['http'] = http_proxy
+ req = requests.get(url,
+ headers=headers,
+ proxies=proxies,
+ timeout=CONNECT_TIMEOUT)
+ # max_retries=3
+ assert req.status_code < 300
+ result = req.content
+ LOG.info('nodes loaded with requests: ' + str(url))
+ return result
+ except Exception as ex:
+ LOG.error('TOX Downloading error with requests: ' + str(ex))
+ # drop through
+
+ if not settings['proxy_type']: # no proxy
+ LOG.debug('Downloading with urllib no proxy: ' + str(url))
+ try:
+ req = urllib.request.Request(url)
+ req.add_header('Content-Type', 'application/json')
+ response = urllib.request.urlopen(req)
+ result = response.read()
+ LOG.info('nodes loaded with no proxy: ' + str(url))
+ return result
+ except Exception as ex:
+ LOG.error('TOX Downloading ' + str(ex))
+ return ''
+
+ return ''
diff --git a/tox_wrapper/tests/support_onions.py b/tox_wrapper/tests/support_onions.py
new file mode 100644
index 0000000..324c889
--- /dev/null
+++ b/tox_wrapper/tests/support_onions.py
@@ -0,0 +1,573 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+import getpass
+import os
+import re
+import select
+import shutil
+import socket
+import sys
+import time
+from typing import Union, Callable, Union
+
+if False:
+ import cepa as stem
+ from cepa.connection import MissingPassword
+ from cepa.control import Controller
+ from cepa.util.tor_tools import is_valid_fingerprint
+else:
+ import stem
+ from stem.connection import MissingPassword
+ from stem.control import Controller
+ from stem.util.tor_tools import is_valid_fingerprint
+
+global LOG
+import logging
+import warnings
+
+warnings.filterwarnings('ignore')
+LOG = logging.getLogger()
+
+bHAVE_TORR = shutil.which('tor-resolve')
+
+yKNOWN_ONIONS = """
+ - facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd # facebook
+ - duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad # ddg
+ - zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad # hks
+"""
+# grep -B 1 '
bool:
+ # FixMe: Linux only
+ sFile = f"/proc/{os.getpid()}/net/route"
+ if not os.path.isfile(sFile): return None
+ i = 0
+ for elt in open(sFile, "r").readlines():
+ if elt.startswith('Iface'): continue
+ if elt.startswith('lo'): continue
+ i += 1
+ return i > 0
+
+def sMapaddressResolv(target:str, iPort:int = 9051, log_level:int = 10) -> str:
+ if not stem:
+ LOG.warn('please install the stem Python package')
+ return ''
+
+ try:
+ controller = oGetStemController(log_level=log_level)
+
+ map_dict = {"0.0.0.0": target}
+ map_ret = controller.map_address(map_dict)
+
+ return map_ret
+ except Exception as e:
+ LOG.exception(e)
+ return ''
+
+def vwait_for_controller(controller, wait_boot:int = 10) -> None:
+ if bAreWeConnected() is False:
+ raise SystemExit("we are not connected")
+ percent = i = 0
+ # You can call this while boostrapping
+ while percent < 100 and i < wait_boot:
+ bootstrap_status = controller.get_info("status/bootstrap-phase")
+ progress_percent = re.match('.* PROGRESS=([0-9]+).*', bootstrap_status)
+ percent = int(progress_percent.group(1))
+ LOG.info(f"Bootstrapping {percent}%")
+ time.sleep(5)
+ i += 5
+
+def bin_to_hex(raw_id:int, length: Union[int, None] = None) -> str:
+ if length is None: length = len(raw_id)
+ res = ''.join('{:02x}'.format(raw_id[i]) for i in range(length))
+ return res.upper()
+
+def lIntroductionPoints(controller=None, lOnions:list = [], itimeout:int = 120, log_level:int = 10):
+ """now working !!! stem 1.8.x timeout must be huge >120
+ 'Provides the descriptor for a hidden service. The **address** is the
+ '.onion' address of the hidden service '
+ What about Services?
+ """
+ try:
+ from cryptography.utils import int_from_bytes
+ except ImportError:
+ import cryptography.utils
+
+ # guessing - not in the current cryptography but stem expects it
+ def int_from_bytes(**args): return int.to_bytes(*args)
+ cryptography.utils.int_from_bytes = int_from_bytes
+ # this will fai if the trick above didnt work
+ from stem.prereq import is_crypto_available
+ is_crypto_available(ed25519=True)
+
+ from queue import Empty
+
+ from stem import Timeout
+ from stem.client.datatype import LinkByFingerprint
+ from stem.descriptor.hidden_service import HiddenServiceDescriptorV3
+
+ if type(lOnions) not in [set, tuple, list]:
+ lOnions = list(lOnions)
+ if controller is None:
+ controller = oGetStemController(log_level=log_level)
+ l = []
+ for elt in lOnions:
+ LOG.info(f"controller.get_hidden_service_descriptor {elt}")
+ try:
+ desc = controller.get_hidden_service_descriptor(elt,
+ await_result=True,
+ timeout=itimeout)
+ # LOG.log(40, f"{dir(desc)} get_hidden_service_descriptor")
+ # timeouts 20 sec
+ # mistakenly a HSv2 descriptor
+ hs_address = HiddenServiceDescriptorV3.from_str(str(desc)) # reparse as HSv3
+ oInnerLayer = hs_address.decrypt(elt)
+ # LOG.log(40, f"{dir(oInnerLayer)}")
+
+ # IntroductionPointV3
+ n = oInnerLayer.introduction_points
+ if not n:
+ LOG.warn(f"NO introduction points for {elt}")
+ continue
+ LOG.info(f"{elt} {len(n)} introduction points")
+ lp = []
+ for introduction_point in n:
+ for linkspecifier in introduction_point.link_specifiers:
+ if isinstance(linkspecifier, LinkByFingerprint):
+ # LOG.log(40, f"Getting fingerprint for {linkspecifier}")
+ if hasattr(linkspecifier, 'fingerprint'):
+ assert len(linkspecifier.value) == 20
+ lp += [bin_to_hex(linkspecifier.value)]
+ LOG.info(f"{len(lp)} introduction points for {elt}")
+ l += lp
+ except (Empty, Timeout,) as e: # noqa
+ LOG.warn(f"Timed out getting introduction points for {elt}")
+ except stem.DescriptorUnavailable as e:
+ LOG.error(e)
+ except Exception as e:
+ LOG.exception(e)
+ return l
+
+def zResolveDomain(domain:str) -> int:
+ try:
+ ip = sTorResolve(domain)
+ except Exception as e: # noqa
+ ip = ''
+ if ip == '':
+ try:
+ lpair = getaddrinfo(domain, 443)
+ except Exception as e:
+ LOG.warn(f"{e}")
+ lpair = None
+ if lpair is None:
+ LOG.warn(f"TorResolv and getaddrinfo failed for {domain}")
+ return ''
+ ip = lpair[0]
+ return ip
+
+def sTorResolve(target:str,
+ verbose:bool = False,
+ sHost:str = '127.0.0.1',
+ iPort:int = 9050,
+ SOCK_TIMEOUT_SECONDS:float = 10.0,
+ SOCK_TIMEOUT_TRIES:int = 3,
+ ) -> str:
+ MAX_INFO_RESPONSE_PACKET_LENGTH = 8
+ if '@' in target:
+ LOG.warn(f"sTorResolve failed invalid hostname {target}")
+ return ''
+ target = target.strip('/')
+ seb = b"\x04\xf0\x00\x00\x00\x00\x00\x01\x00"
+ seb += bytes(target, 'US-ASCII') + b"\x00"
+ assert len(seb) == 10 + len(target), str(len(seb)) + repr(seb)
+
+# LOG.debug(f"0 Sending {len(seb)} to The TOR proxy {seb}")
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((sHost, iPort))
+
+ sock.settimeout(SOCK_TIMEOUT_SECONDS)
+ oRet = sock.sendall(seb) # noqa
+
+ i = 0
+ data = ''
+ while i < SOCK_TIMEOUT_TRIES:
+ i += 1
+ time.sleep(3)
+ lReady = select.select([sock.fileno()], [], [],
+ SOCK_TIMEOUT_SECONDS)
+ if not lReady[0]: continue
+ try:
+ flags=socket.MSG_WAITALL
+ data = sock.recv(MAX_INFO_RESPONSE_PACKET_LENGTH, flags)
+ except socket.timeout:
+ LOG.warn(f"4 The TOR proxy {(sHost, iPort)}" \
+ +" didnt reply in " + str(SOCK_TIMEOUT_SECONDS) + " sec."
+ +" #" +str(i))
+ except Exception as e:
+ LOG.error("4 The TOR proxy " \
+ +repr((sHost, iPort)) \
+ +" errored with " + str(e)
+ +" #" +str(i))
+ sock.close()
+ return ''
+ else:
+ if len(data) > 0: break
+
+ if len(data) == 0:
+ if i > SOCK_TIMEOUT_TRIES:
+ sLabel = "5 No reply #"
+ else:
+ sLabel = "5 No data #"
+ LOG.warn(f"sTorResolve: {sLabel} {i} on {sHost}:{iPort}")
+ sock.close()
+ return ''
+
+ assert len(data) >= 8
+ packet_sf = data[1]
+ if packet_sf == 90:
+ # , "%d" % packet_sf
+ assert f"{packet_sf}" == "90", f"packet_sf = {packet_sf}"
+ return f"{data[4]}.{data[5]}.{data[6]}.{data[7]}"
+ else:
+ # 91
+ LOG.warn(f"tor-resolve failed for {target} on {sHost}:{iPort}")
+
+ os.system(f"tor-resolve -4 {target} > /tmp/e 2>/dev/null")
+# os.system("strace tor-resolve -4 "+target+" 2>&1|grep '^sen\|^rec'")
+
+ return ''
+
+def getaddrinfo(sHost:str, sPort:str) -> list:
+ # do this the explicit way = Ive seen the compact connect fail
+ # >>> sHost, sPort = 'l27.0.0.1', 33446
+ # >>> sock.connect((sHost, sPort))
+ # socket.gaierror: [Errno -2] Name or service not known
+ try:
+ lElts = socket.getaddrinfo(sHost, int(sPort), socket.AF_INET)
+ lElts = list(filter(lambda elt: elt[1] == socket.SOCK_DGRAM, lElts))
+ assert len(lElts) == 1, repr(lElts)
+ lPair = lElts[0][-1]
+ assert len(lPair) == 2, repr(lPair)
+ assert type(lPair[1]) == int, repr(lPair)
+ except (socket.gaierror, OSError, BaseException) as e:
+ LOG.error(e)
+ return None
+ return lPair
+
+def icheck_torrc(sFile:str, oArgs) -> int:
+ l = open(sFile, 'rt').readlines()
+ a = {}
+ for elt in l:
+ elt = elt.strip()
+ if not elt or ' ' not in elt: continue
+ (k, v,) = elt.split(' ', 1)
+ a[k] = v
+ keys = a
+
+ if 'HashedControlPassword' not in keys:
+ LOG.info('Add HashedControlPassword for security')
+ print('run: tor --hashcontrolpassword ')
+ if 'ExcludeExitNodes' in keys:
+ elt = 'BadNodes.ExcludeExitNodes.BadExit'
+ LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}")
+ print(f"move to the {elt} section as a list")
+ if 'GuardNodes' in keys:
+ elt = 'GoodNodes.GuardNodes'
+ LOG.warn(f"Remove GuardNodes and move then to {oArgs.good_nodes}")
+ print(f"move to the {elt} section as a list")
+ if 'ExcludeNodes' in keys:
+ elt = 'BadNodes.ExcludeNodes.BadExit'
+ LOG.warn(f"Remove ExcludeNodes and move then to {oArgs.bad_nodes}")
+ print(f"move to the {elt} section as a list")
+ if 'ControlSocket' not in keys and os.path.exists('/run/tor/control'):
+ LOG.info('Add ControlSocket /run/tor/control for us')
+ print('ControlSocket /run/tor/control GroupWritable RelaxDirModeCheck')
+ if 'UseMicrodescriptors' not in keys or keys['UseMicrodescriptors'] != '1':
+ LOG.info('Add UseMicrodescriptors 0 for us')
+ print('UseMicrodescriptors 0')
+ if 'AutomapHostsSuffixes' not in keys:
+ LOG.info('Add AutomapHostsSuffixes for onions')
+ print('AutomapHostsSuffixes .exit,.onion')
+ if 'AutoMapHostsOnResolve' not in keys:
+ LOG.info('Add AutoMapHostsOnResolve for onions')
+ print('AutoMapHostsOnResolve 1')
+ if 'VirtualAddrNetworkIPv4' not in keys:
+ LOG.info('Add VirtualAddrNetworkIPv4 for onions')
+ print('VirtualAddrNetworkIPv4 172.16.0.0/12')
+ return 0
+
+def lExitExcluder(oArgs, iPort:int = 9051, log_level:int = 10) -> list:
+ """
+ https://raw.githubusercontent.com/nusenu/noContactInfo_Exit_Excluder/main/exclude_noContactInfo_Exits.py
+ """
+ if not stem:
+ LOG.warn('please install the stem Python package')
+ return ''
+ LOG.debug('lExcludeExitNodes')
+
+ try:
+ controller = oGetStemController(log_level=log_level)
+ # generator
+ relays = controller.get_server_descriptors()
+ except Exception as e:
+ LOG.error(f'Failed to get relay descriptors {e}')
+ return None
+
+ if controller.is_set('ExcludeExitNodes'):
+ LOG.info('ExcludeExitNodes is in use already.')
+ return None
+
+ exit_excludelist=[]
+ LOG.debug("Excluded exit relays:")
+ for relay in relays:
+ if relay.exit_policy.is_exiting_allowed() and not relay.contact:
+ if is_valid_fingerprint(relay.fingerprint):
+ exit_excludelist.append(relay.fingerprint)
+ LOG.debug("https://metrics.torproject.org/rs.html#details/%s" % relay.fingerprint)
+ else:
+ LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint)
+
+ try:
+ controller.set_conf('ExcludeExitNodes', exit_excludelist)
+ LOG.info('Excluded a total of %s exit relays without ContactInfo from the exit position.' % len(exit_excludelist))
+ except Exception as e:
+ LOG.exception('ExcludeExitNodes ' +str(e))
+ return exit_excludelist
+
+if __name__ == '__main__':
+ target = 'duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad'
+ controller = oGetStemController(log_level=10)
+ lIntroductionPoints(controller, [target], itimeout=120)
diff --git a/tox_wrapper/tests/support_testing.py b/tox_wrapper/tests/support_testing.py
new file mode 100644
index 0000000..3d88741
--- /dev/null
+++ b/tox_wrapper/tests/support_testing.py
@@ -0,0 +1,970 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+import argparse
+import contextlib
+import inspect
+import json
+import logging
+import os
+import re
+import select
+import shutil
+import socket
+import sys
+import time
+import traceback
+import unittest
+from ctypes import *
+from random import Random
+import functools
+from typing import Union, Callable, Union
+
+random = Random()
+
+try:
+ import coloredlogs
+ if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ:
+ os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red'
+ # https://pypi.org/project/coloredlogs/
+except ImportError as e:
+ coloredlogs = False
+try:
+ import stem
+except ImportError as e:
+ stem = False
+try:
+ import nmap
+except ImportError as e:
+ nmap = False
+
+import tox_wrapper
+import tox_wrapper.toxcore_enums_and_consts as enums
+
+from tox_wrapper.tests.support_http import bAreWeConnected
+from tox_wrapper.tests.support_onions import (is_valid_fingerprint,
+ lIntroductionPoints,
+ oGetStemController,
+ sMapaddressResolv, sTorResolve)
+
+try:
+ from user_data.settings import get_user_config_path
+except ImportError:
+ get_user_config_path = None
+
+# LOG=util.log
+global LOG
+LOG = logging.getLogger()
+
+def LOG_ERROR(l:str) -> None: print('ERRORc: '+l)
+def LOG_WARN(l:str) -> None: print('WARNc: ' +l)
+def LOG_INFO(l:str) -> None: print('INFOc: ' +l)
+def LOG_DEBUG(l:str) -> None: print('DEBUGc: '+l)
+def LOG_TRACE(l:str) -> None: pass # print('TRACE+ '+l)
+
+try:
+ from trepan.api import debug
+ from trepan.interfaces import server as Mserver
+except:
+# print('trepan3 TCP server NOT available.')
+ pass
+else:
+# print('trepan3 TCP server available.')
+ def trepan_handler(num=None, f=None):
+ connection_opts={'IO': 'TCP', 'PORT': 6666}
+ intf = Mserver.ServerInterface(connection_opts=connection_opts)
+ dbg_opts = {'interface': intf }
+ print(f'Starting TCP server listening on port 6666.')
+ debug(dbg_opts=dbg_opts)
+ return
+
+# self._audio_thread.isAlive
+iTHREAD_TIMEOUT = 1
+iTHREAD_SLEEP = 1
+iTHREAD_JOINS = 8
+iNODES = 6
+
+lToxSamplerates = [8000, 12000, 16000, 24000, 48000]
+lToxSampleratesK = [8, 12, 16, 24, 48]
+lBOOLEANS = [
+ 'local_discovery_enabled',
+ 'udp_enabled',
+ 'ipv6_enabled',
+ 'trace_enabled',
+ 'compact_mode',
+ 'allow_inline',
+ 'notifications',
+ 'sound_notifications',
+ 'calls_sound',
+ 'hole_punching_enabled',
+ 'dht_announcements_enabled',
+ 'save_history',
+ 'download_nodes_list'
+ 'core_logging',
+ ]
+
+sDIR = os.environ.get('TMPDIR', '/tmp')
+sTOX_VERSION = "1000002018"
+bHAVE_NMAP = shutil.which('nmap')
+bHAVE_JQ = shutil.which('jq')
+bHAVE_BASH = shutil.which('bash')
+bHAVE_TORR = shutil.which('tor-resolve')
+
+lDEAD_BS = [
+ # Failed to resolve "tox3.plastiras.org"
+ "tox3.plastiras.org",
+ 'tox.kolka.tech',
+ # here and gone
+ '122-116-39-151.hinet-ip.hinet.net',
+ # IPs that do not reverse resolve
+ '49.12.229.145',
+ "46.101.197.175",
+ '114.35.245.150',
+ '172.93.52.70',
+ '195.123.208.139',
+ '205.185.115.131',
+ # IPs that do not rreverse resolve
+ 'yggnode.cf', '188.225.9.167',
+ '85-143-221-42.simplecloud.ru', '85.143.221.42',
+ # IPs that do not ping
+ '104.244.74.69', 'tox.plastiras.org',
+ '195.123.208.139',
+ 'gt.sot-te.ch', '32.226.5.82',
+ # suspicious IPs
+ 'tox.abilinski.com', '172.103.164.250', '172.103.164.250.tpia.cipherkey.com',
+ ]
+
+def assert_main_thread() -> None:
+ from PyQt5 import QtCore, QtWidgets
+ from qtpy.QtWidgets import QApplication
+
+ # this "instance" method is very useful!
+ app_thread = QtWidgets.QApplication.instance().thread()
+ curr_thread = QtCore.QThread.currentThread()
+ if app_thread != curr_thread:
+ raise RuntimeError('attempt to call MainWindow.append_message from non-app thread')
+
+@contextlib.contextmanager
+def ignoreStdout() -> None:
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ old_stdout = os.dup(1)
+ sys.stdout.flush()
+ os.dup2(devnull, 1)
+ os.close(devnull)
+ try:
+ yield
+ finally:
+ os.dup2(old_stdout, 1)
+ os.close(old_stdout)
+
+@contextlib.contextmanager
+def ignoreStderr() -> None:
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ old_stderr = os.dup(2)
+ sys.stderr.flush()
+ os.dup2(devnull, 2)
+ os.close(devnull)
+ try:
+ yield
+ finally:
+ os.dup2(old_stderr, 2)
+ os.close(old_stderr)
+
+def clean_booleans(oArgs) -> None:
+ for key in lBOOLEANS:
+ if not hasattr(oArgs, key): continue
+ val = getattr(oArgs, key)
+ if type(val) == bool: continue
+ if val in ['False', 'false', '0']:
+ setattr(oArgs, key, False)
+ else:
+ setattr(oArgs, key, True)
+
+def on_log(iTox, level, filename, line, func, message, *data) -> None:
+ # LOG.debug(repr((level, filename, line, func, message,)))
+ tox_log_cb(level, filename, line, func, message)
+
+def tox_log_cb(level, filename, line, func, message, *args) -> None:
+ """
+ * @param level The severity of the log message.
+ * @param filename The source file from which the message originated.
+ * @param line The source line from which the message originated.
+ * @param func The function from which the message originated.
+ * @param message The log message.
+ * @param user_data The user data pointer passed to tox_new in options.
+ """
+ if type(func) == bytes:
+ func = str(func, 'utf-8')
+ message = str(message, 'UTF-8')
+ filename = str(filename, 'UTF-8')
+
+ if filename == 'network.c':
+ if line == 660: return
+ # root WARNING 3network.c#944:b'send_packet'attempted to send message with network family 10 (probably IPv6) on IPv4 socket
+ if line == 944: return
+ i = message.find('07 = GET_NODES')
+ if i > 0:
+ return
+ if filename == 'TCP_common.c': return
+
+ i = message.find(' | ')
+ if i > 0:
+ message = message[:i]
+ # message = filename +'#' +str(line) +':'+func +' '+message
+
+ name = 'core'
+ # old level is meaningless
+ level = 10 # LOG.level
+
+ # LOG._log(LOG.level, f"{level}: {message}", list())
+
+ i = message.find('(0: OK)')
+ if i > 0:
+ level = 10 # LOG.debug
+ else:
+ i = message.find('(1: ')
+ if i > 0:
+ level = 30 # LOG.warn
+ else:
+ level = 20 # LOG.info
+
+ o = LOG.makeRecord(filename, level, func, line, message, list(), None)
+ # LOG.handle(o)
+ LOG_TRACE(f"{level}: {func}{line} {message}")
+ return
+
+ elif level == 1:
+ LOG.critical(f"{level}: {message}")
+ elif level == 2:
+ LOG.error(f"{level}: {message}")
+ elif level == 3:
+ LOG.warn(f"{level}: {message}")
+ elif level == 4:
+ LOG.info(f"{level}: {message}")
+ elif level == 5:
+ LOG.debug(f"{level}: {message}")
+ else:
+ LOG_TRACE(f"{level}: {message}")
+
+def vAddLoggerCallback(tox_options, callback=None) -> None:
+ if callback is None:
+ tox_wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback(
+ tox_options._options_pointer,
+ POINTER(None)())
+ tox_options.self_logger_cb = None
+ return
+
+ c_callback = CFUNCTYPE(None, c_void_p, c_int, c_char_p, c_int, c_char_p, c_char_p, c_void_p)
+ tox_options.self_logger_cb = c_callback(callback)
+ tox_wrapper.tox.Tox.libtoxcore.tox_options_set_log_callback(
+ tox_options._options_pointer,
+ tox_options.self_logger_cb)
+
+def get_video_indexes() -> list:
+ # Linux
+ return [str(l[5:]) for l in os.listdir('/dev/') if l.startswith('video')]
+
+def get_audio():
+ with ignoreStderr():
+ import pyaudio
+ oPyA = pyaudio.PyAudio()
+
+ input_devices = output_devices = 0
+ for i in range(oPyA.get_device_count()):
+ device = oPyA.get_device_info_by_index(i)
+ if device["maxInputChannels"]:
+ input_devices += 1
+ if device["maxOutputChannels"]:
+ output_devices += 1
+ # {'index': 21, 'structVersion': 2, 'name': 'default', 'hostApi': 0, 'maxInputChannels': 64, 'maxOutputChannels': 64, 'defaultLowInputLatency': 0.008707482993197279, 'defaultLowOutputLatency': 0.008707482993197279, 'defaultHighInputLatency': 0.034829931972789115, 'defaultHighOutputLatency': 0.034829931972789115, 'defaultSampleRate': 44100.0}
+ audio = {'input': oPyA.get_default_input_device_info()['index'] if input_devices else -1,
+ 'output': oPyA.get_default_output_device_info()['index'] if output_devices else -1,
+ 'enabled': input_devices and output_devices}
+ return audio
+
+def oToxygenToxOptions(oArgs):
+ data = None
+ tox_options = tox_wrapper.tox.Tox.options_new()
+ if oArgs.proxy_type:
+ tox_options.contents.proxy_type = int(oArgs.proxy_type)
+ tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8')
+ tox_options.contents.proxy_port = int(oArgs.proxy_port)
+ tox_options.contents.udp_enabled = False
+ else:
+ tox_options.contents.udp_enabled = oArgs.udp_enabled
+ if not os.path.exists('/proc/sys/net/ipv6'):
+ oArgs.ipv6_enabled = False
+ else:
+ tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled
+
+ tox_options.contents.tcp_port = int(oArgs.tcp_port)
+ tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled
+ tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled
+
+ # overrides
+ tox_options.contents.local_discovery_enabled = False
+ tox_options.contents.experimental_thread_safety = False
+ # REQUIRED!!
+ if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'):
+ LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled))
+ tox_options.contents.ipv6_enabled = False
+ else:
+ tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled)
+
+ if data: # load existing profile
+ tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE']
+ tox_options.contents.savedata_data = c_char_p(data)
+ tox_options.contents.savedata_length = len(data)
+ else: # create new profile
+ tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE']
+ tox_options.contents.savedata_data = None
+ tox_options.contents.savedata_length = 0
+
+ #? tox_options.contents.log_callback = LOG
+ if tox_options._options_pointer:
+ # LOG.debug("Adding logging to tox_options._options_pointer ")
+ vAddLoggerCallback(tox_options, on_log)
+ else:
+ LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer))
+
+ return tox_options
+
+def oMainArgparser(_=None, iMode=0):
+ # 'Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0'
+ if not os.path.exists('/proc/sys/net/ipv6'):
+ bIpV6 = 'False'
+ else:
+ bIpV6 = 'True'
+ lIpV6Choices=[bIpV6, 'False']
+
+ sNodesJson = os.path.join(os.environ['HOME'], '.config', 'tox', 'DHTnodes.json')
+ if not os.path.exists(sNodesJson): sNodesJson = ''
+
+ logfile = os.path.join(os.environ.get('TMPDIR', '/tmp'), 'toxygen.log')
+ if not os.path.exists(logfile): logfile = ''
+
+ parser = argparse.ArgumentParser(add_help=True)
+ parser.add_argument('--proxy_host', '--proxy-host', type=str,
+ # oddball - we want to use '' as a setting
+ default='0.0.0.0',
+ help='proxy host')
+ parser.add_argument('--proxy_port', '--proxy-port', default=0, type=int,
+ help='proxy port')
+ parser.add_argument('--proxy_type', '--proxy-type', default=0, type=int,
+ choices=[0,1,2],
+ help='proxy type 1=http, 2=socks')
+ parser.add_argument('--tcp_port', '--tcp-port', default=0, type=int,
+ help='tcp port')
+ parser.add_argument('--udp_enabled', type=str, default='True',
+ choices=['True', 'False'],
+ help='En/Disable udp')
+ parser.add_argument('--ipv6_enabled', type=str, default=bIpV6,
+ choices=lIpV6Choices,
+ help=f"En/Disable ipv6 - default {bIpV6}")
+ parser.add_argument('--trace_enabled',type=str,
+ default='True' if os.environ.get('DEBUG') else 'False',
+ choices=['True','False'],
+ help='Debugging from toxcore logger_trace or env DEBUG=1')
+ parser.add_argument('--download_nodes_list', type=str, default='False',
+ choices=['True', 'False'],
+ help='Download nodes list')
+ parser.add_argument('--nodes_json', type=str,
+ default=sNodesJson)
+ parser.add_argument('--network', type=str,
+ choices=['main', 'local'],
+ default='main')
+ parser.add_argument('--download_nodes_url', type=str,
+ default='https://nodes.tox.chat/json')
+ parser.add_argument('--logfile', default=logfile,
+ help='Filename for logging - start with + for stdout too')
+ parser.add_argument('--loglevel', default=logging.INFO, type=int,
+ # choices=[logging.info,logging.trace,logging.debug,logging.error]
+ help='Threshold for logging (lower is more) default: 20')
+ parser.add_argument('--mode', type=int, default=iMode,
+ choices=[0,1,2],
+ help='Mode: 0=chat 1=chat+audio 2=chat+audio+video default: 0')
+ parser.add_argument('--hole_punching_enabled',type=str,
+ default='False', choices=['True','False'],
+ help='En/Enable hole punching')
+ parser.add_argument('--dht_announcements_enabled',type=str,
+ default='True', choices=['True','False'],
+ help='En/Disable DHT announcements')
+# argparse.ArgumentError: argument --save_history: conflicting option string: --save_history
+# parser.add_argument('--save_history', type=str, default='True',
+# choices=['True', 'False'],
+# help='En/Disable saving history')
+ return parser
+
+def vSetupLogging(oArgs) -> None:
+ global LOG
+ logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S')
+ logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S'
+ logging._defaultFormatter.default_msec_format = ''
+
+ add = None
+ kwargs = dict(level=oArgs.loglevel,
+ format='%(levelname)-8s %(message)s')
+ if oArgs.logfile:
+ add = oArgs.logfile.startswith('+')
+ sub = oArgs.logfile.startswith('-')
+ if add or sub:
+ oArgs.logfile = oArgs.logfile[1:]
+ kwargs['filename'] = oArgs.logfile
+
+ if coloredlogs:
+ # https://pypi.org/project/coloredlogs/
+ aKw = dict(level=oArgs.loglevel,
+ logger=LOG,
+ stream=sys.stdout,
+ fmt='%(name)s %(levelname)s %(message)s'
+ )
+ coloredlogs.install(**aKw)
+ if oArgs.logfile:
+ oHandler = logging.FileHandler(oArgs.logfile)
+ LOG.addHandler(oHandler)
+ else:
+ logging.basicConfig(**kwargs)
+ if add:
+ oHandler = logging.StreamHandler(sys.stdout)
+ LOG.addHandler(oHandler)
+
+ LOG.info(f"Setting loglevel to {oArgs.loglevel!s}")
+
+
+def setup_logging(oArgs) -> None:
+ global LOG
+ if coloredlogs:
+ aKw = dict(level=oArgs.loglevel,
+ logger=LOG,
+ fmt='%(name)s %(levelname)s %(message)s')
+ if oArgs.logfile:
+ oFd = open(oArgs.logfile, 'wt')
+ setattr(oArgs, 'log_oFd', oFd)
+ aKw['stream'] = oFd
+ coloredlogs.install(**aKw)
+ if oArgs.logfile:
+ oHandler = logging.StreamHandler(stream=sys.stdout)
+ LOG.addHandler(oHandler)
+ else:
+ aKw = dict(level=oArgs.loglevel,
+ format='%(name)s %(levelname)-4s %(message)s')
+ if oArgs.logfile:
+ aKw['filename'] = oArgs.logfile
+ logging.basicConfig(**aKw)
+
+ logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S')
+ logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S'
+ logging._defaultFormatter.default_msec_format = ''
+
+ LOG.setLevel(oArgs.loglevel)
+# LOG.trace = lambda l: LOG.log(0, repr(l))
+ LOG.info(f"Setting loglevel to {oArgs.loglevel!s}")
+
+def signal_handler(num, f) -> None:
+ from trepan.api import debug
+ from trepan.interfaces import server as Mserver
+ connection_opts={'IO': 'TCP', 'PORT': 6666}
+ intf = Mserver.ServerInterface(connection_opts=connection_opts)
+ dbg_opts = {'interface': intf}
+ LOG.info('Starting TCP server listening on port 6666.')
+ debug(dbg_opts=dbg_opts)
+ return
+
+def merge_args_into_settings(args:list, settings:dict) -> None:
+ if args:
+ if not hasattr(args, 'audio'):
+ LOG.warn('No audio ' +repr(args))
+ settings['audio'] = getattr(args, 'audio')
+ if not hasattr(args, 'video'):
+ LOG.warn('No video ' +repr(args))
+ settings['video'] = getattr(args, 'video')
+ for key in settings.keys():
+ # proxy_type proxy_port proxy_host
+ not_key = 'not_' +key
+ if hasattr(args, key):
+ val = getattr(args, key)
+ if type(val) == bytes:
+ # proxy_host - ascii?
+ # filenames - ascii?
+ val = str(val, 'UTF-8')
+ settings[key] = val
+ elif hasattr(args, not_key):
+ val = not getattr(args, not_key)
+ settings[key] = val
+ clean_settings(settings)
+ return
+
+def clean_settings(self:dict) -> None:
+ # failsafe to ensure C tox is bytes and Py settings is str
+
+ # overrides
+ self['mirror_mode'] = False
+ self['save_history'] = True
+ # REQUIRED!!
+ if not os.path.exists('/proc/sys/net/ipv6'):
+ LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist')
+ self['ipv6_enabled'] = False
+
+ if 'proxy_type' in self and self['proxy_type'] == 0:
+ self['proxy_host'] = ''
+ self['proxy_port'] = 0
+
+ if 'proxy_type' in self and self['proxy_type'] != 0 and \
+ 'proxy_host' in self and self['proxy_host'] != '' and \
+ 'proxy_port' in self and self['proxy_port'] != 0:
+ if 'udp_enabled' in self and self['udp_enabled']:
+ # We don't currently support UDP over proxy.
+ LOG.info("UDP enabled and proxy set: disabling UDP")
+ self['udp_enabled'] = False
+ if 'local_discovery_enabled' in self and self['local_discovery_enabled']:
+ LOG.info("local_discovery_enabled enabled and proxy set: disabling local_discovery_enabled")
+ self['local_discovery_enabled'] = False
+ if 'dht_announcements_enabled' in self and self['dht_announcements_enabled']:
+ LOG.info("dht_announcements_enabled enabled and proxy set: disabling dht_announcements_enabled")
+ self['dht_announcements_enabled'] = False
+
+ if 'auto_accept_path' in self and \
+ type(self['auto_accept_path']) == bytes:
+ self['auto_accept_path'] = str(self['auto_accept_path'], 'UTF-8')
+
+ LOG.debug("Cleaned settings")
+
+def lSdSamplerates(iDev:int) -> list:
+ try:
+ import sounddevice as sd
+ except ImportError:
+ return []
+ samplerates = (32000, 44100, 48000, 96000, )
+ device = iDev
+ supported_samplerates = []
+ for fs in samplerates:
+ try:
+ sd.check_output_settings(device=device, samplerate=fs)
+ except Exception as e:
+ # LOG.debug(f"Sample rate not supported {fs}" +' '+str(e))
+ pass
+ else:
+ supported_samplerates.append(fs)
+ return supported_samplerates
+
+def _get_nodes_path(oArgs:str):
+ if oArgs and hasattr(oArgs, 'nodes_json') and \
+ oArgs.nodes_json and os.path.isfile(oArgs.nodes_json):
+ default = oArgs.nodes_json
+ elif get_user_config_path:
+ default = os.path.join(get_user_config_path(), 'toxygen_nodes.json')
+ else:
+ # Windwoes
+ default = os.path.join(os.getenv('HOME'), '.config', 'tox', 'toxygen_nodes.json')
+ LOG.debug("_get_nodes_path: " +default)
+ return default
+
+DEFAULT_NODES_COUNT = 8
+
+global aNODES
+aNODES = {}
+
+# @functools.lru_cache(maxsize=12) TypeError: unhashable type: 'Namespace'
+def generate_nodes(oArgs=None,
+ nodes_count:int = DEFAULT_NODES_COUNT,
+ ipv:str = 'ipv4',
+ udp_not_tcp=True) -> dict:
+ global aNODES
+ sKey = ipv
+ sKey += ',0' if udp_not_tcp else ',1'
+ if sKey in aNODES and aNODES[sKey]:
+ return aNODES[sKey]
+ sFile = _get_nodes_path(oArgs)
+ assert os.path.exists(sFile), sFile
+ lNodes = generate_nodes_from_file(sFile,
+ nodes_count=nodes_count,
+ ipv=ipv,
+ udp_not_tcp=udp_not_tcp)
+ assert lNodes
+ aNODES[sKey] = lNodes
+ return aNODES[sKey]
+
+aNODES_CACHE = {}
+def generate_nodes_from_file(sFile:str,
+ nodes_count:int = DEFAULT_NODES_COUNT,
+ ipv:str = 'ipv4',
+ udp_not_tcp:bool = True,
+ ) -> dict:
+ """https://github.com/TokTok/c-toxcore/issues/469
+I had a conversation with @irungentoo on IRC about whether we really need to call tox_bootstrap() when having UDP disabled and why. The answer is yes, because in addition to TCP relays (tox_add_tcp_relay()), toxcore also needs to know addresses of UDP onion nodes in order to work correctly. The DHT, however, is not used when UDP is disabled. tox_bootstrap() function resolves the address passed to it as argument and calls onion_add_bs_node_path() and DHT_bootstrap() functions. Although calling DHT_bootstrap() is not really necessary as DHT is not used, we still need to resolve the address of the DHT node in order to populate the onion routes with onion_add_bs_node_path() call.
+"""
+ global aNODES_CACHE
+
+ key = ipv
+ key += ',0' if udp_not_tcp else ',1'
+ if key in aNODES_CACHE:
+ sorted_nodes = aNODES_CACHE[key]
+ else:
+ if not os.path.exists(sFile):
+ LOG.error("generate_nodes_from_file file not found " +sFile)
+ return []
+ try:
+ with open(sFile, 'rt') as fl:
+ json_nodes = json.loads(fl.read())['nodes']
+ except Exception as e:
+ LOG.error(f"generate_nodes_from_file error {sFile}\n{e}")
+ return []
+ else:
+ LOG.debug("generate_nodes_from_file " +sFile)
+
+ if udp_not_tcp:
+ nodes = [(node[ipv], node['port'], node['public_key'],) for
+ node in json_nodes if node[ipv] != 'NONE' \
+ and node["status_udp"] in [True, "true"]
+ ]
+ else:
+ nodes = []
+ elts = [(node[ipv], node['tcp_ports'], node['public_key'],) \
+ for node in json_nodes if node[ipv] != 'NONE' \
+ and node["status_tcp"] in [True, "true"]
+ ]
+ for (ipv, ports, public_key,) in elts:
+ for port in ports:
+ nodes += [(ipv, port, public_key)]
+ if not nodes:
+ LOG.warn(f'empty generate_nodes from {sFile} {json_nodes!r}')
+ return []
+ sorted_nodes = nodes
+ aNODES_CACHE[key] = sorted_nodes
+
+ random.shuffle(sorted_nodes)
+ if nodes_count is not None and len(sorted_nodes) > nodes_count:
+ sorted_nodes = sorted_nodes[-nodes_count:]
+ LOG.debug(f"generate_nodes_from_file {sFile} len={len(sorted_nodes)}")
+ return sorted_nodes
+
+def tox_bootstrapd_port() -> int:
+ port = 33446
+ sFile = '/etc/tox-bootstrapd.conf'
+ if os.path.exists(sFile):
+ with open(sFile, 'rt') as oFd:
+ for line in oFd.readlines():
+ if line.startswith('port = '):
+ port = int(line[7:])
+ return port
+
+def bootstrap_local(elts:list, lToxes:list, oArgs=None):
+ if os.path.exists('/run/tox-bootstrapd/tox-bootstrapd.pid'):
+ LOG.debug('/run/tox-bootstrapd/tox-bootstrapd.pid')
+ iRet = True
+ else:
+ iRet = os.system("netstat -nle4|grep -q :33")
+ if iRet > 0:
+ LOG.warn(f'bootstraping local No local DHT running')
+ LOG.info(f'bootstraping local')
+ return bootstrap_udp(elts, lToxes, oArgs)
+
+def lDNSClean(l:list) -> list:
+ global lDEAD_BS
+ # list(set(l).difference(set(lDEAD_BS)))
+ return [elt for elt in l if elt not in lDEAD_BS]
+
+def lExitExcluder(oArgs, iPort:int =9051) -> list:
+ """
+ https://raw.githubusercontent.com/nusenu/noContactInfo_Exit_Excluder/main/exclude_noContactInfo_Exits.py
+ """
+ if not stem:
+ LOG.warn('please install the stem Python package')
+ return ''
+ LOG.debug('lExcludeExitNodes')
+
+ try:
+ controller = oGetStemController(log_level=10)
+ # generator
+ relays = controller.get_server_descriptors()
+ except Exception as e:
+ LOG.error(f'Failed to get relay descriptors {e}')
+ return None
+
+ if controller.is_set('ExcludeExitNodes'):
+ LOG.info('ExcludeExitNodes is in use already.')
+ return None
+
+ exit_excludelist=[]
+ LOG.debug("Excluded exit relays:")
+ for relay in relays:
+ if relay.exit_policy.is_exiting_allowed() and not relay.contact:
+ if is_valid_fingerprint(relay.fingerprint):
+ exit_excludelist.append(relay.fingerprint)
+ LOG.debug("https://metrics.torproject.org/rs.html#details/%s" % relay.fingerprint)
+ else:
+ LOG.warn('Invalid Fingerprint: %s' % relay.fingerprint)
+
+ try:
+ controller.set_conf('ExcludeExitNodes', exit_excludelist)
+ LOG.info('Excluded a total of %s exit relays without ContactInfo from the exit position.' % len(exit_excludelist))
+ except Exception as e:
+ LOG.exception('ExcludeExitNodes ' +str(e))
+ return exit_excludelist
+
+aHOSTS = {}
+@functools.lru_cache(maxsize=20)
+def sDNSLookup(host:str) -> str:
+ global aHOSTS
+ ipv = 0
+ if host in lDEAD_BS:
+# LOG.warn(f"address skipped because in lDEAD_BS {host}")
+ return ''
+ if host in aHOSTS:
+ return aHOSTS[host]
+
+ try:
+ s = host.replace('.','')
+ int(s)
+ ipv = 4
+ except:
+ try:
+ s = host.replace(':','')
+ int(s)
+ ipv = 6
+ except: pass
+
+ if ipv > 0:
+# LOG.debug(f"v={ipv} IP address {host}")
+ return host
+
+ LOG.debug(f"sDNSLookup {host}")
+ ip = ''
+ if host.endswith('.tox') or host.endswith('.onion'):
+ if False and stem:
+ ip = sMapaddressResolv(host)
+ if ip: return ip
+
+ ip = sTorResolve(host)
+ if ip: return ip
+
+ if not bHAVE_TORR:
+ LOG.warn(f"onion address skipped because no tor-resolve {host}")
+ return ''
+ try:
+ sout = f"/tmp/TR{os.getpid()}.log"
+ i = os.system(f"tor-resolve -4 {host} > {sout}")
+ if not i:
+ LOG.warn(f"onion address skipped because tor-resolve on {host}")
+ return ''
+ ip = open(sout, 'rt').read()
+ if ip.endswith('failed.'):
+ LOG.warn(f"onion address skipped because tor-resolve failed on {host}")
+ return ''
+ LOG.debug(f"onion address tor-resolve {ip} on {host}")
+ return ip
+ except:
+ pass
+ else:
+ try:
+ ip = socket.gethostbyname(host)
+ LOG.debug(f"host={host} gethostbyname IP address {ip}")
+ if ip:
+ aHOSTS[host] = ip
+ return ip
+ # drop through
+ except:
+ # drop through
+ pass
+
+ if ip == '':
+ try:
+ sout = f"/tmp/TR{os.getpid()}.log"
+ i = os.system(f"dig {host} +timeout=15|grep ^{host}|sed -e 's/.* //'> {sout}")
+ if not i:
+ LOG.warn(f"address skipped because dig failed on {host}")
+ return ''
+ ip = open(sout, 'rt').read().strip()
+ LOG.debug(f"address dig {ip} on {host}")
+ aHOSTS[host] = ip
+ return ip
+ except:
+ ip = host
+ LOG.debug(f'sDNSLookup {host} -> {ip}')
+ if ip and ip != host:
+ aHOSTS[host] = ip
+ return ip
+
+def bootstrap_udp(lelts:list, lToxes:list, oArgs=None) -> None:
+ lelts = lDNSClean(lelts)
+ socket.setdefaulttimeout(15.0)
+ for oTox in lToxes:
+ random.shuffle(lelts)
+ if hasattr(oTox, 'oArgs'):
+ oArgs = oTox.oArgs
+ if hasattr(oArgs, 'contents') and oArgs.contents.proxy_type != 0:
+ lelts = lelts[:1]
+
+# LOG.debug(f'bootstrap_udp DHT bootstraping {oTox.name} {len(lelts)}')
+ for largs in lelts:
+ assert len(largs) == 3
+ host, port, key = largs
+ assert host; assert port; assert key
+ if host in lDEAD_BS: continue
+ ip = sDNSLookup(host)
+ if not ip:
+ LOG.warn(f'bootstrap_udp to host={host} port={port} did not resolve ip={ip}')
+ continue
+
+ if type(port) == str:
+ port = int(port)
+ try:
+ assert len(key) == 64, key
+ # NOT ip
+ oRet = oTox.bootstrap(host,
+ port,
+ key)
+ except Exception as e:
+ if oArgs is None or (
+ hasattr(oArgs, 'contents') and oArgs.contents.proxy_type == 0):
+ pass
+ # LOG.error(f'bootstrap_udp failed to host={host} port={port} {e}')
+ continue
+ if not oRet:
+ LOG.warn(f'bootstrap_udp failed to {host} : {oRet}')
+ elif oTox.self_get_connection_status() != enums.TOX_CONNECTION['NONE']:
+ LOG.info(f'bootstrap_udp to {host} connected')
+ break
+ else:
+# LOG.debug(f'bootstrap_udp to {host} not connected')
+ pass
+
+def bootstrap_tcp(lelts:list, lToxes:list, oArgs=None) -> None:
+ lelts = lDNSClean(lelts)
+ for oTox in lToxes:
+ if hasattr(oTox, 'oArgs'): oArgs = oTox.oArgs
+ random.shuffle(lelts)
+# LOG.debug(f'bootstrap_tcp bootstapping {oTox.name} {len(lelts)}')
+ for (host, port, key,) in lelts:
+ assert host; assert port;assert key
+ if host in lDEAD_BS: continue
+ ip = sDNSLookup(host)
+ if not ip:
+ LOG.warn(f'bootstrap_tcp to {host} did not resolve ip={ip}')
+# continue
+ ip = host
+ if host.endswith('.onion') and stem:
+ l = lIntroductionPoints(host)
+ if not l:
+ LOG.warn(f'bootstrap_tcp to {host} has no introduction points')
+ continue
+ if type(port) == str:
+ port = int(port)
+ try:
+ assert len(key) == 64, key
+ oRet = oTox.add_tcp_relay(ip,
+ port,
+ key)
+ except Exception as e:
+ # The address could not be resolved to an IP address, or the IP address passed was invalid.
+ LOG.warn(f'bootstrap_tcp to {host} : ' +str(e))
+ continue
+ if not oRet:
+ LOG.warn(f'bootstrap_tcp failed to {host} : {oRet}')
+ elif hasattr(oTox, 'mycon_time') and oTox.mycon_time == 1:
+ LOG.debug(f'bootstrap_tcp to {host} not yet connected')
+ elif hasattr(oTox, 'mycon_status') and oTox.mycon_status is False:
+ LOG.debug(f'bootstrap_tcp to {host} not True')
+ elif oTox.self_get_connection_status() != enums.TOX_CONNECTION['NONE']:
+ LOG.info(f'bootstrap_tcp to {host} connected')
+ break
+ else:
+# LOG.debug(f'bootstrap_tcp to {host} but not connected'
+# +f" last={int(oTox.mycon_time)}" )
+ pass
+
+def iNmapInfoNmap(sProt:str, sHost:str, sPort:str, key=None, environ=None, cmd:str = '') -> int:
+ if sHost in ['-', 'NONE']: return 0
+ if not nmap: return 0
+ nmps = nmap.PortScanner
+ if sProt in ['socks', 'socks5', 'tcp4']:
+ prot = 'tcp'
+ cmd = f" -Pn -n -sT -p T:{sPort}"
+ else:
+ prot = 'udp'
+ cmd = f" -Pn -n -sU -p U:{sPort}"
+ LOG.debug(f"iNmapInfoNmap cmd={cmd}")
+ sys.stdout.flush()
+ o = nmps().scan(hosts=sHost, arguments=cmd)
+ aScan = o['scan']
+ ip = list(aScan.keys())[0]
+ state = aScan[ip][prot][sPort]['state']
+ LOG.info(f"iNmapInfoNmap: to {sHost} {state}")
+ return 0
+
+def iNmapInfo(sProt:str, sHost:str, sPort:str, key=None, environ=None, cmd:str = 'nmap'):
+ if sHost in ['-', 'NONE']: return 0
+ sFile = os.path.join("/tmp", f"{sHost}.{os.getpid()}.nmap")
+ if sProt in ['socks', 'socks5', 'tcp4']:
+ cmd += f" -Pn -n -sT -p T:{sPort} {sHost} | grep /tcp "
+ else:
+ cmd += f" -Pn -n -sU -p U:{sPort} {sHost} | grep /udp "
+ LOG.debug(f"iNmapInfo cmd={cmd}")
+ sys.stdout.flush()
+ iRet = os.system(cmd +f" >{sFile} 2>&1 ")
+ LOG.debug(f"iNmapInfo cmd={cmd} iRet={iRet}")
+ if iRet != 0:
+ return iRet
+ assert os.path.exists(sFile), sFile
+ with open(sFile, 'rt') as oFd:
+ l = oFd.readlines()
+ assert len(l)
+ l = [line for line in l if line and not line.startswith('WARNING:')]
+ s = '\n'.join([s.strip() for s in l])
+ LOG.info(f"iNmapInfo: to {sHost}\n{s}")
+ return 0
+
+
+# bootstrap_iNmapInfo(lElts, self._args, sProt)
+def bootstrap_iNmapInfo(lElts:list, oArgs, protocol:str = "tcp4", bIS_LOCAL:bool = False, iNODES:int = iNODES, cmd:str = 'nmap') -> bool:
+ if not bIS_LOCAL and not bAreWeConnected():
+ LOG.warn(f"bootstrap_iNmapInfo not local and NOT CONNECTED")
+ return True
+ if os.environ['USER'] != 'root':
+ LOG.warn(f"bootstrap_iNmapInfo not ROOT USER={os.environ['USER']}")
+ cmd = 'sudo ' +cmd
+
+ lRetval = []
+ LOG.info(f"bootstrap_iNmapInfo testing nmap={nmap} len={len(lElts[:iNODES])}")
+ for elts in lElts[:iNODES]:
+ host, port, key = elts
+ ip = sDNSLookup(host)
+ if not ip:
+ LOG.info(f"bootstrap_iNmapInfo to {host} did not resolve ip={ip}")
+ continue
+ if type(port) == str:
+ port = int(port)
+ iRet = -1
+ try:
+ if not nmap:
+ iRet = iNmapInfo(protocol, ip, port, key, cmd=cmd)
+ else:
+ iRet = iNmapInfoNmap(protocol, ip, port, key)
+ if iRet != 0:
+ LOG.warn('iNmapInfo to ' +repr(host) +' retval=' +str(iRet))
+ lRetval += [False]
+ else:
+ LOG.info('iNmapInfo to ' +repr(host) +' retval=' +str(iRet))
+ lRetval += [True]
+ except Exception as e:
+ LOG.exception('iNmapInfo to {host} : ' +str(e)
+ )
+ lRetval += [False]
+ return any(lRetval)
+
+def caseFactory(cases:list) -> list:
+ """We want the tests run in order."""
+ if len(cases) > 1:
+ ordered_cases = sorted(cases, key=lambda f: inspect.findsource(f)[1])
+ else:
+ ordered_cases = cases
+ return ordered_cases
+
+def suiteFactory(*testcases):
+ """We want the tests run in order."""
+ linen = lambda f: getattr(tc, f).__code__.co_firstlineno
+ lncmp = lambda a, b: linen(a) - linen(b)
+
+ test_suite = unittest.TestSuite()
+ for tc in testcases:
+ test_suite.addTest(unittest.makeSuite(tc, sortUsing=lncmp))
+ return test_suite
diff --git a/tox_wrapper/tests/tests_wrapper.py b/tox_wrapper/tests/tests_wrapper.py
new file mode 100644
index 0000000..6069584
--- /dev/null
+++ b/tox_wrapper/tests/tests_wrapper.py
@@ -0,0 +1,2271 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+#
+# @file tests.py
+# @author Wei-Ning Huang (AZ)
+#
+# Copyright (C) 2013 - 2014 Wei-Ning Huang (AZ)
+# All Rights reserved.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+"""Originaly from https://github.com/oxij/PyTox c-toxcore-02 branch
+which itself was forked from https://github.com/aitjcize/PyTox/
+
+Modified to work with
+"""
+
+import ctypes
+import faulthandler
+import hashlib
+import logging
+import os
+import random
+import re
+import sys
+import threading
+import traceback
+import unittest
+from ctypes import *
+from typing import Union, Callable, Union
+
+faulthandler.enable()
+
+import warnings
+warnings.filterwarnings('ignore')
+
+try:
+ from io import BytesIO
+ import certifi
+ import pycurl
+except ImportError:
+ pycurl = None
+
+from pyannotate_runtime import collect_types
+
+try:
+ import coloredlogs
+ os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red'
+except ImportError as e:
+ logging.log(logging.DEBUG, f"coloredlogs not available: {e}")
+ coloredlogs = None
+
+try:
+ import color_runner
+except ImportError as e:
+ logging.log(logging.DEBUG, f"color_runner not available: {e}")
+ color_runner = None
+
+import tox_wrapper
+import tox_wrapper.toxcore_enums_and_consts as enums
+from tox_wrapper.tox import Tox, UINT32_MAX, ToxError
+
+from tox_wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION,
+ TOX_FILE_CONTROL,
+ TOX_MESSAGE_TYPE,
+ TOX_SECRET_KEY_SIZE,
+ TOX_USER_STATUS)
+
+try:
+ import support_testing as ts
+except ImportError:
+ import tox_wrapper.tests.support_testing as ts
+
+try:
+ from tests.toxygen_tests import test_sound_notification
+ bIS_NOT_TOXYGEN = False
+except ImportError:
+ bIS_NOT_TOXYGEN = True
+
+# from PyQt5 import QtCore
+import time
+
+sleep = time.sleep
+
+global LOG
+LOG = logging.getLogger('TestS')
+if False:
+ def LOG_ERROR(l: str) -> None: LOG.error('+ '+l)
+ def LOG_WARN(l: str) -> None: LOG.warn('+ '+l)
+ def LOG_INFO(l: str) -> None: LOG.info('+ '+l)
+ def LOG_DEBUG(l: str) -> None: LOG.debug('+ '+l)
+ def LOG_TRACE(l: str) -> None: pass # print('+ '+l)
+else:
+ # just print to stdout so there is NO complications from logging.
+ def LOG_ERROR(l: str) -> None: print('EROR+ '+l)
+ def LOG_WARN(l: str) -> None: print('WARN+ '+l)
+ def LOG_INFO(l: str) -> None: print('INFO+ '+l)
+ def LOG_DEBUG(l: str) -> None: print('DEBUG+ '+l)
+ def LOG_TRACE(l: str) -> None: pass # print('TRAC+ '+l)
+
+ADDR_SIZE = 38 * 2
+CLIENT_ID_SIZE = 32 * 2
+THRESHOLD = 35 # >25
+iN = 6
+
+global oTOX_OPTIONS
+oTOX_OPTIONS = {}
+
+bIS_LOCAL = 'new' in sys.argv or 'local' in sys.argv or 'newlocal' in sys.argv
+bUSE_NOREQUEST = None
+
+def expectedFailure(test_item):
+ test_item.__unittest_expecting_failure__ = True
+ return test_item
+
+def expectedFail(reason: str):
+ """
+ expectedFailure with a reason
+ """
+ def decorator(test_item):
+ test_item.__unittest_expecting_failure__ = True
+ return test_item
+ return decorator
+
+class ToxOptions():
+ def __init__(self):
+ self.ipv6_enabled = True
+ self.udp_enabled = True
+ self.proxy_type = 0
+ self.proxy_host = ''
+ self.proxy_port = 0
+ self.start_port = 0
+ self.end_port = 0
+ self.tcp_port = 0
+ self.savedata_type = 0 # 1=toxsave, 2=secretkey
+ self.savedata_data = b''
+ self.savedata_length = 0
+ self.local_discovery_enabled = False
+ self.dht_announcements_enabled = True
+ self.hole_punching_enabled = False
+ self.experimental_thread_safety = False
+
+class App():
+ def __init__(self):
+ self.mode = 0
+oAPP = App()
+
+class AliceTox(Tox):
+
+ def __init__(self, opts, app=None):
+
+ super(AliceTox, self).__init__(opts, app=app)
+ self._address = self.self_get_address()
+ self.name = 'alice'
+ self._opts = opts
+ self._app = app
+
+class BobTox(Tox):
+
+ def __init__(self, opts, app=None):
+ super(BobTox, self).__init__(opts, app=app)
+ self._address = self.self_get_address()
+ self.name = 'bob'
+ self._opts = opts
+ self._app = app
+
+class BaseThread(threading.Thread):
+
+ def __init__(self, name=None, target=None):
+ if name:
+ super().__init__(name=name, target=target)
+ else:
+ super().__init__(target=target)
+ self._stop_thread = False
+ self.name = name
+
+ def stop_thread(self, timeout: int = -1) -> None:
+ self._stop_thread = True
+ if timeout < 0:
+ timeout = ts.iTHREAD_TIMEOUT
+ i = 0
+ while i < ts.iTHREAD_JOINS:
+ self.join(timeout)
+ if not self.is_alive(): break
+ i = i + 1
+ else:
+ LOG.warning(f"{self.name} BLOCKED")
+
+class ToxIterateThread(BaseThread):
+
+ def __init__(self, tox):
+ super().__init__(name='ToxIterateThread')
+ self._tox = tox
+
+ def run(self) -> None:
+ while not self._stop_thread:
+ self._tox.iterate()
+ sleep(self._tox.iteration_interval() / 1000)
+
+global bob, alice
+bob = alice = None
+
+def prepare(self):
+ global bob, alice
+ def bobs_on_self_connection_status(iTox, connection_state, *args) -> None:
+ status = connection_state
+ self.bob.dht_connected = status
+ self.bob.mycon_time = time.time()
+ try:
+ if status != TOX_CONNECTION['NONE']:
+ LOG_DEBUG(f"bobs_on_self_connection_status TRUE {status}" \
+ +f" last={int(self.bob.mycon_time)}" )
+ self.bob.mycon_status = True
+ else:
+ LOG_DEBUG(f"bobs_on_self_connection_status FALSE {status}" \
+ +f" last={int(self.bob.mycon_time)}" )
+ self.bob.mycon_status = False
+ except Exception as e:
+ LOG_ERROR(f"bobs_on_self_connection_status {e}")
+ else:
+ if self.bob.self_get_connection_status() != status:
+ LOG_WARN(f"bobs_on_self_connection_status DISAGREE {status}")
+
+ def alices_on_self_connection_status(iTox, connection_state: int, *args) -> None:
+ #FixMe connection_num
+ status = connection_state
+ self.alice.dht_connected = status
+ self.alice.mycon_time = time.time()
+ try:
+ if status != TOX_CONNECTION['NONE']:
+ LOG_DEBUG(f"alices_on_self_connection_status TRUE {status}" \
+ +f" last={int(self.alice.mycon_time)}" )
+ self.alice.mycon_status = True
+ else:
+ LOG_WARN(f"alices_on_self_connection_status FALSE {status}" \
+ +f" last={int(self.alice.mycon_time)}" )
+ self.alice.mycon_status = False
+ except Exception as e:
+ LOG_ERROR(f"alices_on_self_connection_status error={e}")
+ self.alice.dht_connected = status
+
+ opts = oToxygenToxOptions(oTOX_OARGS)
+ global bUSE_NOREQUEST
+ bUSE_NOREQUEST = oTOX_OARGS.norequest == 'True'
+
+ alice = AliceTox(opts, app=oAPP)
+ alice.oArgs = opts
+ alice.dht_connected = -1
+ alice.mycon_status = False
+ alice.mycon_time = 1
+ alice.callback_self_connection_status(alices_on_self_connection_status)
+
+ bob = BobTox(opts, app=oAPP)
+ bob.oArgs = opts
+ bob.dht_connected = -1
+ bob.mycon_status = False
+ bob.mycon_time = 1
+ bob.callback_self_connection_status(bobs_on_self_connection_status)
+ if not bIS_LOCAL and not ts.bAreWeConnected():
+ LOG.warning(f"doOnce not local and NOT CONNECTED")
+ return [bob, alice]
+
+class ToxSuite(unittest.TestCase):
+ failureException = AssertionError
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ global oTOX_OARGS
+ assert oTOX_OPTIONS
+ assert oTOX_OARGS
+
+ cls.lUdp = ts.generate_nodes(
+ oArgs=oTOX_OARGS,
+ nodes_count=2*ts.iNODES,
+ ipv='ipv4',
+ udp_not_tcp=True)
+
+ cls.lTcp = ts.generate_nodes(
+ oArgs=oTOX_OARGS,
+ nodes_count=2*ts.iNODES,
+ ipv='ipv4',
+ udp_not_tcp=False)
+
+ def tearDown(self) -> None:
+ """
+ """
+ if hasattr(self, 'bob') and self.bob.self_get_friend_list_size() >= 1:
+ LOG.warn(f"tearDown BOBS STILL HAS A FRIEND LIST {self.bob.self_get_friend_list()}")
+ for elt in self.bob.self_get_friend_list():
+ self.bob.friend_delete(elt)
+ if hasattr(self, 'alice') and self.alice.self_get_friend_list_size() >= 1:
+ LOG.warn(f"tearDown ALICE STILL HAS A FRIEND LIST {self.alice.self_get_friend_list()}")
+ for elt in self.alice.self_get_friend_list():
+ self.alice.friend_delete(elt)
+
+ LOG.debug(f"tearDown threads={threading.active_count()}")
+ if hasattr(self, 'bob'):
+ bob.callback_self_connection_status(None)
+ if hasattr(self.bob, 'main_loop'):
+ self.bob._main_loop.stop_thread()
+ del self.bob._main_loop
+# self.bob.kill()
+ del self.bob
+ if hasattr(self, 'alice'):
+ alice.callback_self_connection_status(None)
+ if hasattr(self.alice, 'main_loop'):
+ self.alice._main_loop.stop_thread()
+ del self.alice._main_loop
+# self.alice.kill()
+ del self.alice
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ if hasattr(cls, 'bob'):
+ cls.bob._main_loop.stop_thread()
+ cls.bob.kill()
+ del cls.bob
+ if hasattr(cls, 'alice'):
+ cls.alice._main_loop.stop_thread()
+ cls.alice.kill()
+ del cls.alice
+
+ def bBobNeedAlice(self) -> bool:
+ """
+ """
+ if hasattr(self, 'baid') and self.baid >= 0 and \
+ self.baid in self.bob.self_get_friend_list():
+ LOG.warn(f"setUp ALICE IS ALREADY IN BOBS FRIEND LIST")
+ return False
+ elif self.bob.self_get_friend_list_size() >= 1:
+ LOG.warn(f"setUp BOB STILL HAS A FRIEND LIST")
+ return False
+ return True
+
+ def bAliceNeedAddBob (self) -> bool:
+ if hasattr(self, 'abid') and self.abid >= 0 and \
+ self.abid in self.alice.self_get_friend_list():
+ LOG.warn(f"setUp BOB IS ALREADY IN ALICES FRIEND LIST")
+ return False
+ elif self.alice.self_get_friend_list_size() >= 1:
+ LOG.warn(f"setUp ALICE STILL HAS A FRIEND LIST")
+ return False
+ return True
+
+ def setUp(self):
+ cls = self
+ if not hasattr(cls, 'alice') and not hasattr(cls, 'bob'):
+ l = prepare(cls)
+ assert l
+ cls.bob, cls.alice = l
+ if not hasattr(cls.bob, '_main_loop'):
+#? cls.bob._main_loop = ToxIterateThread(cls.bob)
+#? cls.bob._main_loop.start()
+ LOG.debug(f"cls.bob._main_loop: ") # {threading.enumerate()}
+ if not hasattr(cls.alice, '_main_loop'):
+#? cls.alice._main_loop = ToxIterateThread(cls.alice)
+#? cls.alice._main_loop.start()
+ LOG.debug(f"cls.alice._main_loop: ") # {threading.enumerate()}
+
+ self.bBobNeedAlice()
+ self.bAliceNeedAddBob()
+
+ def run(self, result=None) -> None:
+ """ Stop after first error """
+ if not result.errors:
+ super(ToxSuite, self).run(result)
+
+ def get_connection_status(self) -> None:
+ if self.bob.mycon_time <= 1 or self.alice.mycon_time <= 1:
+ pass
+ # drop through
+ elif self.bob.dht_connected == TOX_CONNECTION['NONE']:
+ return False
+ elif self.alice.dht_connected == TOX_CONNECTION['NONE']:
+ return False
+
+ # if not self.connected
+ if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']:
+ return False
+ if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']:
+ return False
+ return True
+
+ def loop(self, n) -> None:
+ """
+ t:iterate
+ t:iteration_interval
+ """
+ interval = self.bob.iteration_interval()
+ for i in range(n):
+ self.alice.iterate()
+ self.bob.iterate()
+ sleep(interval / 1000.0)
+
+ def call_bootstrap(self, num: Union[int, None] = None, lToxes:list[int] =None, i:int =0) -> None:
+ if num == None: num=ts.iNODES
+ if lToxes is None:
+ lToxes = [self.alice, self.bob]
+# LOG.debug(f"call_bootstrap network={oTOX_OARGS.network}")
+ if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']:
+ ts.bootstrap_local(self.lUdp, lToxes)
+ elif not ts.bAreWeConnected():
+ LOG.warning('we are NOT CONNECTED')
+ else:
+ random.shuffle(self.lUdp)
+ if oTOX_OARGS.proxy_port > 0:
+ lElts = self.lUdp[:1]
+ else:
+ lElts = self.lUdp[:num+i]
+ LOG.debug(f"call_bootstrap ts.bootstrap_udp {len(lElts)}")
+ ts.bootstrap_udp(lElts, lToxes)
+ random.shuffle(self.lTcp)
+ lElts = self.lTcp[:num+i]
+ LOG.debug(f"call_bootstrap ts.bootstrap_tcp {len(lElts)}")
+ ts.bootstrap_tcp(lElts, lToxes)
+
+ def group_until_connected(self, otox, group_number:int, num: Union[int, None] = None, iMax:int = THRESHOLD) -> None:
+ """
+ """
+ i = 0
+ bRet = None
+ while i <= iMax :
+ iRet = otox.group_is_connected(group_number)
+ if iRet == True or iRet == 0:
+ bRet = True
+ break
+ if i % 5 == 0:
+ j = i//5
+ self.call_bootstrap(num, lToxes=None, i=j)
+ s = ''
+ if i == 0: s = '\n'
+ LOG.info(s+"group_until_connected " \
+ +" #" + str(i) \
+ +" iRet=" +repr(iRet) \
+ +f" BOBS={otox.mycon_status}" \
+ +f" last={int(otox.mycon_time)}" )
+ i += 1
+ self.loop(100)
+ else:
+ bRet = False
+
+ if bRet:
+ LOG.info(f"group_until_connected True i={i}" \
+ +f" iMax={iMax}" \
+ +f" BOB={otox.self_get_connection_status()}" \
+ +f" last={int(otox.mycon_time)}" )
+ return True
+ else:
+ LOG.warning(f"group_until_connected False i={i}" \
+ +f" iMax={iMax}" \
+ +f" BOB={otox.self_get_connection_status()}" \
+ +f" last={int(otox.mycon_time)}" )
+ return False
+
+ def loop_until_connected(self, num: Union[int, None] = None) -> None:
+ """
+ t:on_self_connection_status
+ t:self_get_connection_status
+ """
+ global THRESHOLD
+ i = 0
+ bRet = None
+ while i <= THRESHOLD :
+ if (self.alice.mycon_status and self.bob.mycon_status):
+ bRet = True
+ break
+ if i % 5 == 0:
+ j = i//5
+ self.call_bootstrap(num, lToxes=None, i=j)
+ s = ''
+ if i == 0: s = '\n'
+ LOG.info(s+"loop_until_connected " \
+ +" #" + str(i) \
+ +" BOB=" +repr(self.bob.self_get_connection_status()) \
+ +" ALICE=" +repr(self.alice.self_get_connection_status())
+ +f" BOBS={self.bob.mycon_status}" \
+ +f" ALICES={self.alice.mycon_status}" \
+ +f" last={int(self.bob.mycon_time)}" )
+ if (self.alice.mycon_status and self.bob.mycon_status):
+ bRet = True
+ break
+ if (self.alice.self_get_connection_status() and
+ self.bob.self_get_connection_status()):
+ LOG_WARN(f"loop_until_connected disagree status() DISAGREE" \
+ +f' self.bob.mycon_status={self.bob.mycon_status}' \
+ +f' alice.mycon_status={self.alice.mycon_status}' \
+ +f" last={int(self.bob.mycon_time)}" )
+ bRet = True
+ break
+ i += 1
+ self.loop(100)
+ else:
+ bRet = False
+
+ if bRet or \
+ ( self.bob.self_get_connection_status() != TOX_CONNECTION['NONE'] and \
+ self.alice.self_get_connection_status() != TOX_CONNECTION['NONE'] ):
+ LOG.info(f"loop_until_connected returning True {i}" \
+ +f" BOB={self.bob.self_get_connection_status()}" \
+ +f" ALICE={self.alice.self_get_connection_status()}" \
+ +f" last={int(self.bob.mycon_time)}" )
+ return True
+ else:
+ THRESHOLD += 5
+ LOG.warning(f"loop_until_connected returning False {i}" \
+ +f" BOB={self.bob.self_get_connection_status()}" \
+ +f" ALICE={self.alice.self_get_connection_status()}" \
+ +f" last={int(self.bob.mycon_time)}" )
+ return False
+
+ def wait_objs_attr(self, objs: list, attr: str) -> bool:
+ global THRESHOLD
+ i = 0
+ while i <= THRESHOLD:
+ if i % 5 == 0:
+ num = None
+ j = 0
+ j = i//5
+ self.call_bootstrap(num, objs, i=j)
+ LOG.debug(f"wait_objs_attr {objs} for {attr} {i}")
+ if all([getattr(obj, attr) for obj in objs]):
+ return True
+ self.loop(100)
+ i += 1
+ else:
+ THRESHOLD += 1
+ LOG.warn(f"wait_objs_attr for {attr} i >= {THRESHOLD}")
+
+ return all([getattr(obj, attr) is not None for obj in objs])
+
+ def wait_otox_attrs(self, obj, attrs: list[str]) -> bool:
+ assert all(attrs), f"wait_otox_attrs {attrs}"
+ i = 0
+ while i <= THRESHOLD:
+ if i % 5 == 0:
+ num = None
+ j = 0
+ if obj.mycon_time == 1:
+ num = 4
+ j = i//5
+ if obj.self_get_connection_status() == TOX_CONNECTION['NONE']:
+ self.call_bootstrap(num, [obj], i=j)
+ LOG.debug(f"wait_otox_attrs {obj.name} for {attrs} {i}" \
+ +f" last={int(obj.mycon_time)}")
+ if all([getattr(obj, attr) is not None for attr in attrs]):
+ return True
+ self.loop(100)
+ i += 1
+ else:
+ LOG.warning(f"wait_otox_attrs i >= {THRESHOLD} results={[getattr(obj, attr) for attr in attrs]}")
+
+ return all([getattr(obj, attr) for attr in attrs])
+
+ def wait_ensure_exec(self, method, args:list) -> bool:
+ i = 0
+ oRet = None
+ while i <= THRESHOLD:
+ if i % 5 == 0:
+ j = i//5
+ self.call_bootstrap(num=None, lToxes=None, i=j)
+ LOG.debug("wait_ensure_exec " \
+ +" " +str(method)
+ +" " +str(i))
+ try:
+ oRet = method(*args)
+ if oRet:
+ LOG.info(f"wait_ensure_exec oRet {oRet!r}")
+ return True
+ except ArgumentError as e:
+ # ArgumentError('This client is currently NOT CONNECTED to the friend.')
+ # dunno
+ LOG.warning(f"wait_ensure_exec ArgumentError {e}")
+ return False
+ except Exception as e:
+ LOG.warning(f"wait_ensure_exec EXCEPTION {e}")
+ return False
+ sleep(3)
+ i += 1
+ else:
+ LOG.error(f"wait_ensure_exec i >= {1*THRESHOLD}")
+ return False
+
+ return oRet
+
+ def bob_add_alice_as_friend_norequest(self) -> bool:
+ if not self.bBobNeedAlice(): return True
+
+ MSG = 'Hi, this is Bob.'
+ iRet = self.bob.friend_add_norequest(self.alice._address)
+ if iRet < 0:
+ return False
+ self.baid = self.bob.friend_by_public_key(self.alice._address)
+ assert self.baid >= 0, self.baid
+ assert self.bob.friend_exists(self.baid), "bob.friend_exists"
+ assert not self.bob.friend_exists(self.baid + 1)
+ assert self.baid in self.bob.self_get_friend_list()
+ assert self.bob.self_get_friend_list_size() >= 1
+ return True
+
+ def alice_add_bob_as_friend_norequest(self) -> bool:
+ if not self.bAliceNeedAddBob(): return True
+
+ iRet = self.alice.friend_add_norequest(self.bob._address)
+ if iRet < 0:
+ return False
+ self.abid = self.alice.friend_by_public_key(self.bob._address)
+ assert self.abid >= 0, self.abid
+ assert self.abid in self.alice.self_get_friend_list()
+ assert self.alice.friend_exists(self.abid), "alice.friend_exists"
+ assert not self.alice.friend_exists(self.abid + 1)
+ assert self.alice.self_get_friend_list_size() >= 1
+ return True
+
+ def both_add_as_friend(self) -> bool:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend()
+ assert self.alice_add_bob_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend_norequest()
+ assert self.alice_add_bob_as_friend_norequest()
+ if not hasattr(self, 'baid') or self.baid < 0:
+ LOG.warn("both_add_as_friend no bob, baid")
+ if not hasattr(self, 'abid') or self.abid < 0:
+ LOG.warn("both_add_as_friend no alice, abid")
+ return True
+
+ def both_add_as_friend_norequest(self) -> bool:
+ if self.bBobNeedAlice():
+ assert self.bob_add_alice_as_friend_norequest()
+ if self.bAliceNeedAddBob():
+ assert self.alice_add_bob_as_friend_norequest()
+ if not hasattr(self, 'baid') or self.baid < 0:
+ LOG.warn("both_add_as_friend_norequest no bob, baid")
+ if not hasattr(self, 'abid') or self.abid < 0:
+ LOG.warn("both_add_as_friend_norequest no alice, abid")
+
+ #: Test last online
+#? assert self.alice.friend_get_last_online(self.abid) is not None
+#? assert self.bob.friend_get_last_online(self.baid) is not None
+ return True
+
+ def bob_add_alice_as_friend(self) -> bool:
+ """
+ t:friend_add
+ t:on_friend_request
+ t:friend_by_public_key
+ """
+ MSG = 'Alice, this is Bob.'
+ sSlot = 'friend_request'
+ if not self.bBobNeedAlice(): return True
+
+ def alices_on_friend_request(iTox,
+ public_key,
+ message_data,
+ message_data_size,
+ *largs) -> None:
+ LOG_DEBUG(f"alices_on_friend_request: " +repr(message_data))
+ try:
+ assert str(message_data, 'UTF-8') == MSG
+ LOG_INFO(f"alices_on_friend_request: {sSlot} = True ")
+ except Exception as e:
+ LOG_WARN(f"alices_on_friend_request: EXCEPTION {e}")
+ # return
+ setattr(self.bob, sSlot, True)
+
+ setattr(self.bob, sSlot, None)
+ inum = -1
+ try:
+ inum = self.bob.friend_add(self.alice._address, bytes(MSG, 'UTF-8'))
+ assert inum >= 0, f"bob_add_alice_as_friend !>= 0 {inum}"
+ self.alice.callback_friend_request(alices_on_friend_request)
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ LOG_WARN(f"bob_add_alice_as_friend NO {sSlot}")
+ # return False
+ self.baid = self.bob.friend_by_public_key(self.alice._address)
+ assert self.baid >= 0, self.baid
+ assert self.bob.friend_exists(self.baid)
+ assert not self.bob.friend_exists(self.baid + 1)
+ assert self.bob.self_get_friend_list_size() >= 1
+ assert self.baid in self.bob.self_get_friend_list()
+ except Exception as e:
+ LOG.error(f"bob_add_alice_as_friend EXCEPTION {e}")
+ return False
+ finally:
+ self.bob.callback_friend_message(None)
+
+ return True
+
+ def alice_add_bob_as_friend(self) -> bool:
+ """
+ t:friend_add
+ t:on_friend_request
+ t:friend_by_public_key
+ """
+ MSG = 'Bob, this is Alice.'
+ sSlot = 'friend_request'
+ if not self.bAliceNeedAddBob(): return True
+
+ def bobs_on_friend_request(iTox,
+ public_key,
+ message_data,
+ message_data_size,
+ *largs) -> None:
+ LOG_DEBUG(f"bobs_on_friend_request: " +repr(message_data))
+ try:
+ assert str(message_data, 'UTF-8') == MSG
+ except Exception as e:
+ LOG_WARN(f"bobs_on_friend_request: Exception {e}")
+ # return
+ setattr(self.alice, sSlot, True)
+
+ LOG_INFO(f"bobs_on_friend_request: {sSlot} = True ")
+ setattr(self.alice, sSlot, None)
+ inum = -1
+ try:
+ inum = self.alice.friend_add(self.bob._address, bytes(MSG, 'UTF-8'))
+ assert inum >= 0, f"alice.friend_add !>= 0 {inum}"
+ self.bob.callback_friend_request(bobs_on_friend_request)
+ if not self.wait_otox_attrs(self.alice, [sSlot]):
+ LOG_WARN(f"alice.friend_add NO wait {sSlot}")
+ #? return False
+ self.abid = self.alice.friend_by_public_key(self.bob._address)
+ assert self.abid >= 0, self.abid
+ assert self.alice.friend_exists(self.abid), "not exists"
+ assert not self.alice.friend_exists(self.abid + 1), "exists +1"
+ assert self.abid in self.alice.self_get_friend_list(), "not in list"
+ assert self.alice.self_get_friend_list_size() >= 1, "list size"
+ except Exception as e:
+ LOG.error(f"alice.friend_add EXCEPTION {e}")
+ return False
+ finally:
+ self.bob.callback_friend_message(None)
+ return True
+
+ def bob_add_alice_as_friend_and_status(self) -> bool:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend()
+
+ #: Wait until both are online
+ sSlot = 'friend_conn_status'
+ setattr(self.bob, sSlot, False)
+ def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs) -> None:
+ LOG_INFO(f"bobs_on_friend_connection_status {friend_id} ?>=0" +repr(iStatus))
+ setattr(self.bob, sSlot, False)
+
+ sSlot = 'friend_status'
+ setattr(self.bob, sSlot, None)
+ def bobs_on_friend_status(iTox, friend_id, iStatus, *largs) -> None:
+ LOG_INFO(f"bobs_on_friend_status {friend_id} ?>=0" +repr(iStatus))
+ setattr(self.bob, sSlot, False)
+
+ sSlot = 'friend_conn_status'
+ setattr(self.alice, sSlot, None)
+ def alices_on_friend_connection_status(iTox, friend_id, iStatus, *largs) -> None:
+ LOG_INFO(f"alices_on_friend_connection_status {friend_id} ?>=0 " +repr(iStatus))
+ setattr(self.alice, sSlot, False)
+
+ sSlot = 'friend_status'
+ setattr(self.alice, sSlot, None)
+ def alices_on_friend_status(iTox, friend_id, iStatus, *largs) -> None:
+ LOG_INFO(f"alices_on_friend_status {friend_id} ?>=0 " +repr(iStatus))
+ setattr(self.alice, sSlot, False)
+
+ try:
+ LOG.info("bob_add_alice_as_friend_and_status waiting for alice connections")
+ if not self.wait_otox_attrs(self.alice,
+ ['friend_conn_status',
+ 'friend_status']):
+ return False
+
+ self.bob.callback_friend_connection_status(bobs_on_friend_connection_status)
+ self.bob.callback_friend_status(bobs_on_friend_status)
+ self.alice.callback_friend_connection_status(alices_on_friend_connection_status)
+ self.alice.callback_friend_status(alices_on_friend_status)
+
+ LOG.info("bob_add_alice_as_friend_and_status waiting for bob connections")
+ if not self.wait_otox_attrs(self.bob,
+ ['friend_conn_status',
+ 'friend_status']):
+ LOG_WARN('bob_add_alice_as_friend_and_status NO')
+ # return False
+ except Exception as e:
+ LOG.error(f"bob_add_alice_as_friend_and_status ERROR {e}")
+ return False
+ finally:
+ self.alice.callback_friend_connection_status(None)
+ self.bob.callback_friend_connection_status(None)
+ self.alice.callback_friend_status(None)
+ self.bob.callback_friend_status(None)
+ return True
+
+ def bob_to_alice_connected(self) -> bool:
+ assert hasattr(self, 'baid')
+ iRet = self.bob.friend_get_connection_status(self.baid)
+ if iRet == TOX_CONNECTION['NONE']:
+ LOG.warn("bob.friend_get_connection_status")
+ return False
+ return True
+
+ def alice_to_bob_connected(self) -> bool:
+ assert hasattr(self, 'abid')
+ iRet = self.alice.friend_get_connection_status(self.abid)
+ if iRet == TOX_CONNECTION['NONE']:
+ LOG.error("alice.friend_get_connection_status")
+ return False
+ return True
+
+ def otox_test_groups_create(self,
+ otox,
+ group_name='test_group',
+ nick='test_nick',
+ topic='Test Topic', # str
+ ) -> int:
+ privacy_state = enums.TOX_GROUP_PRIVACY_STATE['PUBLIC']
+
+ iGrp = otox.group_new(privacy_state, group_name, nick)
+ assert iGrp >= 0
+ LOG.info(f"group iGrp={iGrp}")
+
+ otox.group_set_topic(iGrp, topic)
+ assert otox.group_get_topic(iGrp) == topic
+ assert otox.group_get_topic_size(iGrp) == len(topic)
+
+ name = otox.group_get_name(iGrp)
+ if type(name) == bytes:
+ name = str(name, 'utf-8')
+ assert name == group_name, name
+ assert otox.group_get_name_size(iGrp) == len(group_name)
+
+ sPk = otox.group_self_get_public_key(iGrp)
+ assert otox.group_get_password_size(iGrp) >= 0
+ sP = otox.group_get_password(iGrp)
+ assert otox.group_get_privacy_state(iGrp) == privacy_state
+
+ assert otox.group_get_number_groups() > 0, "numg={otox.group_get_number_groups()}"
+ LOG.info(f"group pK={sPk} iGrp={iGrp} numg={otox.group_get_number_groups()}")
+ return iGrp
+
+ def otox_verify_group(self, otox, iGrp) -> int:
+ """
+ group_self_get_name
+ group_self_get_peer_id
+ group_self_get_public_key
+ group_self_get_role
+ group_self_get_status
+ group_self_set_name
+ """
+
+ group_number = iGrp
+ try:
+ assert type(iGrp) == int, "otox_test_groups_join iGrp not an int"
+ assert iGrp < UINT32_MAX, "otox_test_groups_join iGrp failure UINT32_MAX"
+ assert iGrp >= 0, f"otox_test_groups_join iGrp={iGrp} < 0"
+ sGrp = otox.group_get_chat_id(iGrp)
+ assert len(sGrp) == enums.TOX_GROUP_CHAT_ID_SIZE * 2, \
+ f"group sGrp={sGrp} {len(sGrp)} != {enums.TOX_GROUP_CHAT_ID_SIZE * 2}"
+ sPk = otox.group_self_get_public_key(iGrp)
+ LOG.info(f"otox_verify_group sPk={sPk} iGrp={iGrp} n={otox.group_get_number_groups()}")
+
+ sName = otox.group_self_get_name(iGrp)
+ iStat = otox.group_self_get_status(iGrp)
+ iId = otox.group_self_get_peer_id(iGrp)
+ iRole = otox.group_self_get_role(iGrp)
+ iStat = otox.group_self_get_status(iGrp)
+ LOG.info(f"otox_verify_group sName={sName} iStat={iStat} iId={iId} iRole={iRole} iStat={iStat}")
+
+ assert otox.group_self_set_name(iGrp, "NewName")
+
+ bRet = otox.group_is_connected(iGrp)
+ except Exception as e:
+ LOG.warn(f"group_is_connected EXCEPTION {e}")
+ return -1
+ # chat->connection_state == CS_CONNECTED || chat->connection_state == CS_CONNECTING;
+ if not bRet:
+ LOG.warn(f"group_is_connected WARN not connected iGrp={iGrp} n={otox.group_get_number_groups()}")
+ else:
+ LOG.info(f"group_is_connected SUCCESS connected iGrp={iGrp} n={otox.group_get_number_groups()}")
+ try:
+ bRet = self.group_until_connected(otox, iGrp, iMax=2*THRESHOLD)
+ except Exception as e:
+ LOG.error(f"group_until_connected EXCEPTION {e}")
+ return -1
+ # chat->connection_state == CS_CONNECTED || chat->connection_state == CS_CONNECTING;
+ if bRet:
+ LOG.warn(f"group_until_connected WARN not connected iGrp={iGrp} n={otox.group_get_number_groups()}")
+ else:
+ LOG.info(f"group_until_connected SUCCESS connected iGrp={iGrp} n={otox.group_get_number_groups()}")
+
+ message = bytes('hello', 'utf-8')
+ bRet = otox.group_send_message(iGrp, TOX_MESSAGE_TYPE['NORMAL'], message)
+ if not bRet:
+ LOG.warn(f"group_send_message {bRet}")
+ else:
+ LOG.debug(f"group_send_message {bRet}")
+
+ # 360497DA684BCE2A500C1AF9B3A5CE949BBB9F6FB1F91589806FB04CA039E313
+ # 75D2163C19FEFFE51508046398202DDC321E6F9B6654E99BAE45FFEC134F05DE
+ def otox_test_groups_join(self, otox,
+ chat_id="75d2163c19feffe51508046398202ddc321e6f9b6654e99bae45ffec134f05de",
+ nick='nick',
+ topic='Test Topic', # str
+ ):
+ status = ''
+ password = ''
+ LOG.debug(f"group_join nick={nick} chat_id={chat_id}")
+ try:
+ iGrp = otox.group_join(chat_id, password, nick, status)
+ LOG.info(f"otox_test_groups_join SUCCESS iGrp={iGrp} chat_id={chat_id}")
+ self.otox_verify_group(otox, iGrp)
+
+ except Exception as e:
+ # gui
+ LOG.error(f"otox_test_groups_join EXCEPTION {e}")
+ raise
+
+ return iGrp
+
+ def otox_test_groups(self,
+ otox,
+ group_name='test_group',
+ nick='test_nick',
+ topic='Test Topic', # str
+ ) -> int:
+
+ try:
+ iGrp = self.otox_test_groups_create(otox, group_name, nick, topic)
+ self.otox_verify_group(otox, iGrp)
+ except Exception as e:
+ LOG.error(f"otox_test_groups ERROR {e}")
+ raise
+
+ # unfinished
+ # tox_group_peer_exit_cb
+ # tox_callback_group_peer_join
+ # tox.callback_group_peer_status
+ # tox.callback_group_peer_name
+ # tox.callback_group_peer_exit
+ # tox.callback_group_peer_join
+ return iGrp
+
+ def wait_friend_get_connection_status(self, otox, fid:int, n:int = iN) -> int:
+ i = 0
+ while i < n:
+ iRet = otox.friend_get_connection_status(fid)
+ if iRet == TOX_CONNECTION['NONE']:
+# LOG.debug(f"wait_friend_get_connection_status NOT CONNECTED i={i} {iRet}")
+ self.loop_until_connected()
+ else:
+ LOG.info("wait_friend_get_connection_status {iRet}")
+ return True
+ i += 1
+ else:
+ LOG.error(f"wait_friend_get_connection_status n={n}")
+ return False
+
+ def warn_if_no_cb(self, alice, sSlot:str) -> None:
+ if not hasattr(alice, sSlot+'_cb') or \
+ not getattr(alice, sSlot+'_cb'):
+ LOG.warning(f"self.bob.{sSlot}_cb NOT EXIST")
+
+ def warn_if_cb(self, alice, sSlot:str) -> None:
+ if hasattr(self.bob, sSlot+'_cb') and \
+ getattr(self.bob, sSlot+'_cb'):
+ LOG.warning(f"self.bob.{sSlot}_cb EXIST")
+
+ # tests are executed in order
+ def test_notice_log(self) -> None: # works
+ notice = '/var/lib/tor/.SelekTOR/3xx/cache/9050/notice.log'
+ if os.path.exists(notice):
+ iRet = os.system(f"sudo sed -e '1,/.notice. Bootstrapped 100%/d' {notice}" + \
+ "| grep 'Tried for 120 seconds to get a connection to :0.'")
+ if iRet == 0:
+ raise SystemExit("seconds to get a connection to :0")
+ else:
+ LOG.debug(f"checked {notice}")
+
+ def test_tests_logging(self): # works
+ with self.assertLogs('foo', level='INFO') as cm:
+ logging.getLogger('foo').info('first message')
+ logging.getLogger('foo.bar').error('second message')
+ logging.getLogger('foo.bar.baz').debug('third message')
+ self.assertEqual(cm.output, ['INFO:foo:first message',
+ 'ERROR:foo.bar:second message'])
+
+ def test_hash(self): # works
+ otox = self.bob
+ string = 'abcdef'
+ name = otox.hash(bytes(string, 'utf-8'))
+ assert name
+ string = b'abcdef'
+ name = otox.hash(string)
+ assert name
+ LOG.info(f"test_hash: {string} -> {name} ")
+
+ def test_tests_start(self) -> None: # works
+ """
+ t:hash
+ t:kill
+ t:libtoxcore
+ t:options_default
+ t:options_free
+ t:options_new
+ t:self_get_toxid
+ """
+ LOG.info("test_tests_start " )
+ port = ts.tox_bootstrapd_port()
+
+ assert len(self.bob._address) == 2*TOX_ADDRESS_SIZE, len(self.bob._address)
+ assert len(self.alice._address) == 2*TOX_ADDRESS_SIZE, \
+ len(self.alice._address)
+
+ assert self.bob.self_get_address() == self.bob._address
+ assert self.alice.self_get_address() == self.alice._address
+
+ def test_bootstrap_local_netstat(self) -> None: # works
+ """
+ t:callback_file_chunk_request
+ t:callback_file_recv
+ t:callback_file_recv_chunk
+ t:callback_file_recv_control
+ t:callback_friend_connection_status
+ t:callback_friend_lossless_packet
+ t:callback_friend_lossy_packet
+ t:callback_friend_message
+ t:callback_friend_name
+ t:callback_friend_read_receipt
+ t:callback_friend_request
+ t:callback_friend_status
+ t:callback_friend_status_message
+ t:callback_friend_typing
+ t:callback_group_custom_packet
+ t:callback_group_invite
+ """
+ if oTOX_OARGS.network not in ['new', 'newlocal', 'local']:
+ return
+
+ port = ts.tox_bootstrapd_port()
+ if not port:
+ return
+ iStatus = os.system(f"""netstat -nle4 | grep :{port}""")
+ if iStatus == 0:
+ LOG.info(f"bootstrap_local_netstat port {port} iStatus={iStatus}")
+ else:
+ LOG.warning(f"bootstrap_local_netstat NOT {port} iStatus={iStatus}")
+
+ def test_bootstrap_local(self) -> None: # works
+ """
+ t:call_bootstrap
+ t:add_tcp_relay
+ t:self_get_dht_id
+ """
+ # get port from /etc/tox-bootstrapd.conf 33445
+ self.call_bootstrap()
+ # ts.bootstrap_local(self, self.lUdp)
+ i = 0
+ iStatus = -1
+ while i < 10:
+ i = i + 1
+ iStatus = self.bob.self_get_connection_status()
+ if iStatus != TOX_CONNECTION['NONE']:
+ break
+ sleep(3)
+ else:
+ pass
+
+ o1 = self.alice.self_get_dht_id()
+ assert len(o1) == 64
+ o2 = self.bob.self_get_dht_id()
+ assert len(o2) == 64
+
+# if o1 != o2: LOG.warning(f"bootstrap_local DHT NOT same {o1} {o2} iStatus={iStatus}")
+
+ iStatus = self.bob.self_get_connection_status()
+ if iStatus != TOX_CONNECTION['NONE']:
+ LOG.info(f"bootstrap_local connected iStatus={iStatus}")
+ return True
+ iStatus = self.alice.self_get_connection_status()
+ if iStatus != TOX_CONNECTION['NONE']:
+ LOG.info(f"bootstrap_local connected iStatus={iStatus}")
+ return True
+ LOG.warning(f"bootstrap_local NOT CONNECTED iStatus={iStatus}")
+ return False
+
+ def test_bootstrap_iNmapInfo(self) -> None: # works
+# if os.environ['USER'] != 'root':
+# return
+ iStatus = self.bob.self_get_connection_status()
+ LOG.info(f"test_bootstrap_iNmapInfo connected bob iStatus={iStatus}")
+ if oTOX_OARGS.network in ['new', 'newlocal', 'localnew']:
+ lElts = self.lUdp
+ elif oTOX_OARGS.proxy_port > 0:
+ lElts = self.lTcp
+ else:
+ lElts = self.lUdp
+ lRetval = []
+ random.shuffle(lElts)
+ # assert
+ ts.bootstrap_iNmapInfo(lElts, oTOX_OARGS, bIS_LOCAL, iNODES=8)
+
+ def test_self_get_secret_key(self) -> None: # works
+ """
+ t:self_get_secret_key
+ """
+ # test_self_get_secret_key
+ CRYPTO_SECRET_KEY_SIZE = 32
+ secret_key = create_string_buffer(CRYPTO_SECRET_KEY_SIZE)
+ oRet0 = self.alice.self_get_secret_key(secret_key)
+ assert oRet0, repr(oRet0)
+ LOG.info('test_self_get_secret_key ' +repr(oRet0))
+ assert len(str(oRet0))
+ del secret_key
+
+ def test_self_get_public_keys(self) -> None: # works
+ """
+ t:self_get_secret_key
+ t:self_get_public_key
+ """
+
+ LOG.info('test_self_get_public_keys self.alice.self_get_secret_key')
+ oRet0 = self.alice.self_get_secret_key()
+ assert len(oRet0)
+ LOG.info('test_self_get_public_keys ' +repr(oRet0))
+ oRet1 = self.alice.self_get_public_key()
+ assert len(oRet1)
+ LOG.info('test_self_get_public_keys ' +repr(oRet1))
+ assert oRet0 != oRet1, repr(oRet0) +' != ' +repr(oRet1)
+
+ def test_self_name(self) -> None: # works
+ """
+ t:self_set_name
+ t:self_get_name
+ t:self_get_name_size
+ """
+ self.alice.self_set_name('Alice')
+ assert self.alice.self_get_name() == 'Alice'
+ assert self.alice.self_get_name_size() == len('Alice')
+ self.bob.self_set_name('Bob')
+ assert self.bob.self_get_name() == 'Bob'
+ assert self.bob.self_get_name_size() == len('Bob')
+
+ @unittest.skip('loud')
+ @unittest.skipIf(bIS_NOT_TOXYGEN or oTOX_OARGS.mode == 0, 'not testing in toxygen')
+ def test_sound_notification(self) -> None: # works
+ """
+ Plays sound notification
+ :param type of notification
+ """
+ from tests.toxygen_tests import test_sound_notification
+ test_sound_notification(self)
+
+ def test_address(self) -> None: # works
+ """
+ t:self_get_address
+ t:self_get_nospam
+ t:self_set_nospam
+ t:self_get_keys
+ """
+ assert len(self.alice.self_get_address()) == ADDR_SIZE
+ assert len(self.bob.self_get_address()) == ADDR_SIZE
+
+ self.alice.self_set_nospam(0x12345678)
+ assert self.alice.self_get_nospam() == 0x12345678
+ self.loop(50)
+
+ if hasattr(self.alice, 'self_get_keys'):
+ pk, sk = self.alice.self_get_keys()
+ assert pk == self.alice.self_get_address()[:CLIENT_ID_SIZE]
+
+ def test_status_message(self) -> None: # works
+ """
+ t:self_get_status_message
+ t:self_get_status_message_size
+ """
+ MSG = 'Happy'
+ self.alice.self_set_status_message(MSG)
+ self.loop(100)
+ assert self.alice.self_get_status_message() == MSG, \
+ self.alice.self_get_status_message() +' is not ' +MSG
+ assert self.alice.self_get_status_message_size() == len(MSG)
+
+ def test_self_get_udp_port(self) -> None: # works
+ """
+ t:self_get_udp_port
+ """
+ if hasattr(oTOX_OPTIONS, 'udp_port') and oTOX_OPTIONS.udp_port:
+ o = self.alice.self_get_udp_port()
+ LOG.info('self_get_udp_port alice ' +repr(o))
+ assert o > 0
+ o = self.bob.self_get_udp_port()
+ LOG.info('self_get_udp_port bob ' +repr(o))
+ assert o > 0
+
+ def test_self_get_tcp_port(self) -> None: # works
+ """
+ t:self_get_tcp_port
+ """
+ if hasattr(oTOX_OPTIONS, 'tcp_port') and oTOX_OPTIONS.tcp_port:
+ # errors if tcp_port <= 0
+ o = self.alice.self_get_tcp_port()
+ LOG.info('self_get_tcp_port ' +repr(o))
+ o = self.bob.self_get_tcp_port()
+ LOG.info('self_get_tcp_port ' +repr(o))
+
+ def test_get_dht_id(self) -> None: # works
+ """
+ t:self_get_dht_id
+ """
+ o1 = self.alice.self_get_dht_id()
+ assert len(o1) == 64
+ o2 = self.bob.self_get_dht_id()
+ assert len(o2) == 64
+
+ def test_bob_add_alice_as_friend_norequest(self) -> None: # works
+ """
+ t:friend_delete
+ t:friend_exists
+ t:friend_add_norequest
+ t:friend_get_public_key
+ t:self_get_friend_list
+ t:self_get_friend_list_size
+ """
+ i = len(self.bob.self_get_friend_list())
+ assert self.bob_add_alice_as_friend_norequest()
+ assert len(self.bob.self_get_friend_list()) == i + 1
+ #: Test last online
+ assert self.bob.friend_get_last_online(self.baid) is not None
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+
+
+ def test_alice_add_bob_as_friend_norequest(self) -> None: # works - intermittent failures
+ """
+ t:friend_delete
+ t:friend_exists
+ t:friend_get_public_key
+ t:self_get_friend_list
+ t:self_get_friend_list_size
+ """
+ i = len(self.alice.self_get_friend_list())
+ assert self.alice_add_bob_as_friend_norequest()
+ assert len(self.alice.self_get_friend_list()) == i + 1
+ #: Test last online
+ assert self.alice.friend_get_last_online(self.abid) is not None
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ def test_both_add_as_friend_norequest(self) -> None: # works
+ """
+ t:friend_delete
+ t:friend_exists
+ t:friend_get_public_key
+ t:self_get_friend_list
+ t:self_get_friend_list_size
+ """
+ try:
+ self.both_add_as_friend_norequest()
+ assert len(self.bob.self_get_friend_list()) > 0
+ assert len(self.alice.self_get_friend_list()) > 0
+ except AssertionError as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ finally:
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ assert len(self.bob.self_get_friend_list()) == 0
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+ assert len(self.alice.self_get_friend_list()) == 0
+
+ def test_bob_add_alice_as_friend_and_status(self) -> None:
+ """
+ t:friend_delete
+ t:friend_exists
+ t:friend_get_public_key
+ t:self_get_friend_list
+ t:self_get_friend_list_size
+ """
+ self.bob_add_alice_as_friend_and_status()
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+
+ @unittest.skip('unfinished')
+ def test_alice_add_bob_as_friend_and_status(self) -> None:
+ assert self.alice_add_bob_as_friend_and_status()
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ def test_loop_until_connected(self) -> None: # works
+ assert self.loop_until_connected()
+
+ def test_bob_assert_connection_status(self) -> None: # works
+ if self.bob.self_get_connection_status() == TOX_CONNECTION['NONE']:
+ AssertionError("ERROR: NOT CONNECTED " \
+ +repr(self.bob.self_get_connection_status()))
+
+ def test_alice_assert_connection_status(self) -> None: # works
+ if self.alice.self_get_connection_status() == TOX_CONNECTION['NONE']:
+ AssertionError("ERROR: NOT CONNECTED " \
+ +repr(self.alice.self_get_connection_status()))
+
+ def test_bob_assert_mycon_status(self) -> None: # works
+ if self.bob.mycon_status == False:
+ AssertionError("ERROR: NOT CONNECTED " \
+ +repr(self.bob.mycon_status))
+
+ def test_alice_assert_mycon_status(self) -> None: # works
+ if self.alice.mycon_status == False:
+ AssertionError("ERROR: NOT CONNECTED " \
+ +repr(self.alice.mycon_status))
+
+ def test_bob_add_alice_as_friend(self) -> None: # works?
+ try:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend()
+ #: Test last online
+ assert self.bob.friend_get_last_online(self.baid) is not None
+ except AssertionError as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ finally:
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if len(self.bob.self_get_friend_list()) > 0:
+ LOG.warn(f"WTF bob.self_get_friend_list() {bob.self_get_friend_list()}")
+
+ def test_alice_add_bob_as_friend(self) -> None: # works!
+ try:
+ if bUSE_NOREQUEST:
+ assert self.alice_add_bob_as_friend_norequest()
+ else:
+ assert self.alice_add_bob_as_friend()
+ #: Test last online
+ assert self.alice.friend_get_last_online(self.abid) is not None
+ except AssertionError as e:
+ #WTF?
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+ LOG.error(f"Failed test {e}")
+ raise
+ except Exception as e:
+ #WTF?
+ LOG.error(f"test_alice_add_bob_as_friend EXCEPTION {e}")
+ raise
+ finally:
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+ if len(self.alice.self_get_friend_list()) > 0:
+ LOG.warn(f"WTF alice.self_get_friend_list() {alice.self_get_friend_list()}")
+
+ def test_both_add_as_friend(self) -> None: # works
+ try:
+ if bUSE_NOREQUEST:
+ assert self.both_add_as_friend_norequest()
+ else:
+ assert self.both_add_as_friend()
+ except AssertionError as e:
+ LOG.warn(f"Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_both_add_as_friend EXCEPTION {e}")
+ raise
+ finally:
+ if hasattr(self,'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self,'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ def test_groups_join(self) -> None:
+ """
+ t:group_join
+ t:group_disconnect
+ t:group_leave
+ t:group_self_set_name
+ """
+ if not self.get_connection_status():
+ LOG.warning(f"test_groups_join NOT CONNECTED")
+ self.loop_until_connected()
+
+ iGrp = self.otox_test_groups_join(self.bob)
+ LOG.info(f"test_groups_join iGrp={iGrp}")
+ assert iGrp >= 0, f"test_groups_join iGrp={iGrp}"
+ try:
+ self.bob.group_disconnect(iGrp)
+ except Exception as e:
+ LOG.error(f"bob.group_disconnect EXCEPTION {e}")
+ raise
+ try:
+ self.bob.group_leave(iGrp, None)
+ except Exception as e:
+ LOG.error(f"bob.group_leave EXCEPTION {e}")
+ raise
+
+ def test_groups(self) -> None:
+ """
+ t:group_new
+ t:group_disconnect
+ t:group_get_name
+ t:group_get_name_size
+ t:group_get_topic
+ t:group_get_topic_size
+ t:group_get_privacy_state
+ t:group_self_set_name
+ t:group_get_number_groups
+
+ t:group_founder_set_password
+ t:group_founder_set_peer_limit
+ t:group_founder_set_privacy_state
+ t:group_get_chat_id
+ t:group_get_password
+ t:group_get_password_size
+ t:group_get_peer_limit
+ t:group_invite_accept
+ t:group_invite_friend
+ t:group_is_connected
+ t:group_leave
+ t:group_mod_set_role
+ """
+ iGrp = self.otox_test_groups(self.bob)
+ LOG.info(f"test_groups iGrp={iGrp}")
+ if iGrp >= 0:
+ try:
+ self.bob.group_disconnect(iGrp)
+ except Exception as e:
+ LOG.error(f"bob.group_disconnect EXCEPTION {e}")
+ raise
+ try:
+ self.bob.group_leave(iGrp, None)
+ except Exception as e:
+ LOG.error(f"bob.group_leave EXCEPTION {e}")
+ raise
+
+#? @unittest.skip("double free or corruption (fasttop)")
+ @expectedFail('fails') # assertion fails on == MSG
+ def test_on_friend_status_message(self) -> None: # fails
+ """
+ t:self_set_status_message
+ t:self_get_status_message
+ t:self_get_status_message_size
+ t:friend_set_status_message
+ t:friend_get_status_message
+ t:friend_get_status_message_size
+ t:on_friend_status_message
+ """
+ MSG = 'Happy'
+ sSlot = 'friend_status_message'
+
+ def bob_on_friend_status_message(iTox, friend_id, new_status_message, new_status_size, *largs) -> None:
+ LOG_INFO(f"BOB_ON_friend_status_message friend_id={friend_id} " \
+ +f"new_status_message={new_status_message}")
+ try:
+ assert str(new_status_message, 'UTF-8') == MSG
+ assert friend_id == self.baid
+ except Exception as e:
+ LOG_ERROR(f"BOB_ON_friend_status_message EXCEPTION {e}")
+ setattr(self.bob, sSlot, True)
+
+ setattr(self.bob, sSlot, None)
+ try:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ assert self.alice_add_bob_as_friend_norequest()
+ else:
+ # no not connected error
+ assert self.bob_add_alice_as_friend()
+ assert self.alice_add_bob_as_friend_norequest()
+
+ self.bob.callback_friend_status_message(bob_on_friend_status_message)
+ self.warn_if_no_cb(self.bob, sSlot)
+ status_message = bytes(MSG, 'utf-8')
+ self.alice.self_set_status_message(status_message)
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ LOG_WARN(f"on_friend_status_message NO {sSlot}")
+
+ assert self.bob.friend_get_status_message(self.baid) == MSG, \
+ f"message={self.bob.friend_get_status_message(self.baid)}"
+ assert self.bob.friend_get_status_message_size(self.baid) == len(MSG), \
+ f"message_len={self.bob.friend_get_status_message_size(self.baid)}"
+
+ except AssertionError as e:
+ LOG.error(f"test_on_friend_status_message FAILED {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_on_friend_status_message EXCEPTION {e}")
+ raise
+ finally:
+ self.bob.callback_friend_status(None)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ def test_friend(self) -> None: # works! sometimes
+ """
+ t:friend_get_name
+ t:friend_get_name_size
+ t:on_friend_name
+ """
+
+ try:
+ #: Test friend request
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ assert self.alice_add_bob_as_friend_norequest()
+ else:
+ # no not connected error
+ assert self.bob_add_alice_as_friend()
+ assert self.alice_add_bob_as_friend_norequest()
+
+ a = self.alice.self_get_address()[:CLIENT_ID_SIZE]
+ assert self.bob.friend_get_public_key(self.baid) == a, \
+ LOG.error(f"test_friend BAID {a}")
+ del a
+
+ #: Test friend_get_public_key
+ b = self.bob.self_get_address()[:CLIENT_ID_SIZE]
+ assert self.alice.friend_get_public_key(self.abid) == b, \
+ LOG.error(f"test_friend ABID {b}")
+ del b
+ except AssertionError as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_friend EXCEPTION {e}")
+ raise
+ finally:
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ @expectedFail('fails') # assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['BUSY']
+ def test_user_status(self) -> None: # fails
+ """
+ t:self_get_status
+ t:self_set_status
+ t:friend_get_status
+ t:friend_get_status
+ t:on_friend_status
+ """
+ sSlot = 'friend_status'
+
+ setattr(self.bob, sSlot, None)
+ def bobs_on_friend_set_status(iTox, friend_id, new_status, *largs) -> None:
+ LOG_INFO(f"bobs_on_friend_set_status {friend_id} {new_status}")
+ try:
+ assert friend_id == self.baid
+ assert new_status in [TOX_USER_STATUS['BUSY'], TOX_USER_STATUS['AWAY']]
+ except Exception as e:
+ LOG_WARN(f"bobs_on_friend_set_status EXCEPTION {e}")
+ setattr(self.bob, sSlot, True)
+
+ try:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend()
+ if not self.get_connection_status():
+ LOG.warning(f"test_user_status NOT CONNECTED self.get_connection_status")
+ self.loop_until_connected()
+
+ self.bob.callback_friend_status(bobs_on_friend_set_status)
+ self.warn_if_no_cb(self.bob, sSlot)
+ sSTATUS = TOX_USER_STATUS['BUSY']
+ self.alice.self_set_status(sSTATUS)
+ sSlot = 'friend_status'
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ # malloc(): unaligned tcache chunk detected
+ LOG_WARN(f'test_user_status NO {sSlot}')
+
+ assert self.bob.friend_get_status(self.baid) == TOX_USER_STATUS['BUSY'], \
+ f"friend_get_status {self.bob.friend_get_status(self.baid)} != {TOX_USER_STATUS['BUSY']}"
+
+ except AssertionError as e:
+ LOG.error(f"test_user_status FAILED {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_user_status EXCEPTION {e}")
+ raise
+ finally:
+ self.bob.callback_friend_status(None)
+ self.warn_if_cb(self.bob, sSlot)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+
+ @unittest.skip('crashes')
+ def test_kill_remake(self) -> None:
+ """
+ t:friend_get_kill_remake
+ t:on_friend_connection_status
+ """
+ sSlot = 'friend_connection_status'
+ setattr(self.bob, sSlot, None)
+ def bobs_on_friend_connection_status(iTox, friend_id, iStatus, *largs):
+ LOG_INFO(f"bobs_on_friend_connection_status " +repr(iStatus))
+ try:
+ assert friend_id == self.baid
+ except Exception as e:
+ LOG_ERROR(f"bobs_on_friend_connection_status ERROR {e}")
+ setattr(self.bob, sSlot, True)
+
+ opts = oToxygenToxOptions(oTOX_OARGS)
+ setattr(self.bob, sSlot, True)
+ try:
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend()
+
+ self.bob.callback_friend_connection_status(bobs_on_friend_connection_status)
+
+ LOG.info("test_kill_remake killing alice")
+ self.alice.kill() #! bang
+ LOG.info("test_kill_remake making alice")
+ self.alice = Tox(opts, app=oAPP)
+ LOG.info("test_kill_remake maked alice")
+
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ LOG_WARN(f'test_kill_remake NO {sSlot}')
+ except AssertionError as e:
+ LOG.error(f"test_kill_remake Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"bobs_on_friend_connection_status {e}")
+ raise
+ finally:
+ self.bob.callback_friend_connection_status(None)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+
+ @expectedFail('fails') # new name is empty
+ def test_friend_name(self) -> None: # works!
+ """
+ t:self_set_name
+ t:friend_get_name
+ t:friend_get_name_size
+ t:on_friend_name
+ """
+
+ sSlot= 'friend_name'
+ #: Test friend request
+
+ #: Test friend name
+ NEWNAME = 'Jenny'
+
+ def bobs_on_friend_name(iTox, fid:int, newname, iNameSize, *largs) -> None:
+ LOG_INFO(f"bobs_on_friend_name {sSlot} {fid}")
+ try:
+ assert fid == self.baid
+ assert str(newname, 'UTF-8') == NEWNAME
+ except Exception as e:
+ LOG_ERROR(f"bobs_on_friend_name EXCEPTION {e}")
+ setattr(self.bob, sSlot, True)
+
+ setattr(self.bob, sSlot, None)
+ try:
+ LOG.info("test_friend_name")
+ if bUSE_NOREQUEST:
+ assert self.bob_add_alice_as_friend_norequest()
+ else:
+ assert self.bob_add_alice_as_friend()
+
+ self.bob.callback_friend_name(bobs_on_friend_name)
+ self.warn_if_no_cb(self.bob, sSlot)
+ self.alice.self_set_name(NEWNAME)
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ LOG_WARN(f"bobs_on_friend_name NO {sSlot}")
+
+ # name=None
+ assert self.bob.friend_get_name(self.baid) == NEWNAME, \
+ f"{self.bob.friend_get_name(self.baid)} != {NEWNAME}"
+ assert self.bob.friend_get_name_size(self.baid) == len(NEWNAME), \
+ f"{self.bob.friend_get_name_size(self.baid)} != {len(NEWNAME)}"
+
+ except AssertionError as e:
+ LOG.error(f"test_friend_name Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_friend EXCEPTION {e}")
+ raise
+ finally:
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ self.bob.callback_friend_name(None)
+ self.warn_if_cb(self.bob, sSlot)
+
+
+ @expectedFail('fails') # This client is currently not connected to the friend.
+ def test_friend_message(self) -> None: # fails
+ """
+ t:on_friend_action
+ t:on_friend_message
+ t:friend_send_message
+ """
+
+ #: Test message
+ MSG = 'Hi, Bob!'
+ sSlot = 'friend_message'
+
+ def alices_on_friend_message(iTox, fid:int, msg_type, message, iSize, *largs) -> None:
+ LOG_DEBUG(f"alices_on_friend_message {fid} {message}")
+ try:
+ assert fid == self.alice.abid
+ assert msg_type == TOX_MESSAGE_TYPE['NORMAL']
+ assert str(message, 'UTF-8') == MSG
+ except Exception as e:
+ LOG_ERROR(f"alices_on_friend_message EXCEPTION {e}")
+ else:
+ LOG_INFO(f"alices_on_friend_message {message}")
+ setattr(self.alice, sSlot, True)
+
+ setattr(self.alice, sSlot, None)
+ self.alice.callback_friend_message(None)
+ try:
+ if bUSE_NOREQUEST:
+ assert self.both_add_as_friend_norequest()
+ else:
+ assert self.both_add_as_friend()
+ assert hasattr(self, 'baid'), \
+ "both_add_as_friend_norequest no bob, baid"
+ assert hasattr(self, 'abid'), \
+ "both_add_as_friend_norequest no alice, abid"
+ if not self.wait_friend_get_connection_status(self.bob, self.baid, n=2*iN):
+ LOG.warn('baid not connected')
+ if not self.wait_friend_get_connection_status(self.alice, self.abid, n=2*iN):
+ LOG.warn('abid not connected')
+ self.alice.callback_friend_message(alices_on_friend_message)
+ self.warn_if_no_cb(self.alice, sSlot)
+
+ # dunno - both This client is currently NOT CONNECTED to the friend.
+ iMesId = self.bob.friend_send_message(self.baid,
+ TOX_MESSAGE_TYPE['NORMAL'],
+ bytes(MSG, 'UTF-8'))
+ assert iMesId >= 0, "iMesId >= 0"
+ if not self.wait_otox_attrs(self.alice, [sSlot]):
+ LOG_WARN(f"alices_on_friend_message NO {sSlot}")
+ except ArgumentError as e:
+ # ArgumentError('This client is currently NOT CONNECTED to the friend.')
+ # dunno
+ LOG.error(f"test_friend_message ArgumentError {e}")
+ raise
+ except AssertionError as e:
+ LOG.error(f"test_friend_message AssertionError {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_friend_message EXCEPTION {e}")
+ raise
+ finally:
+ self.alice.callback_friend_message(None)
+ self.warn_if_cb(self.alice, sSlot)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ # This client is currently not connected to the friend.
+ def test_friend_action(self) -> None: # works! sometimes?
+ """
+ t:on_friend_action
+ t:on_friend_message
+ t:friend_send_message
+ """
+
+ #: Test action
+ ACTION = 'Kick'
+ sSlot = 'friend_read_action'
+ setattr(self.bob, sSlot, None)
+ def UNUSEDtheir_on_friend_action(iTox, fid:int, msg_type, action, *largs):
+ LOG_DEBUG(f"their_on_friend_action {fid} {msg_type} {sSlot} {action}")
+ try:
+ assert msg_type == TOX_MESSAGE_TYPE['ACTION']
+ assert action == ACTION
+ except Exception as e:
+ LOG_ERROR(f"their_on_friend_action EXCEPTION {sSlot} {e}")
+ else:
+ LOG_INFO(f"their_on_friend_action {sSlot} {action}")
+ setattr(self.bob, sSlot, True)
+
+ sSlot = 'friend_read_receipt'
+ setattr(self.alice, sSlot, None)
+ def their_on_read_reciept(iTox, fid:int, msg_id, *largs) -> None:
+ LOG_DEBUG(f"their_on_read_reciept {fid} {msg_id}")
+ sSlot = 'friend_read_receipt'
+ try:
+ # should be the receivers id
+ assert fid == bob.baid or fid == alice.abid
+ assert msg_id >= 0
+ except Exception as e:
+ LOG_ERROR(f"their_on_read_reciept {sSlot} {e}")
+ else:
+ LOG_INFO(f"their_on_read_reciept {sSlot} fid={fid}")
+ setattr(self.alice, sSlot, True)
+
+ try:
+ if bUSE_NOREQUEST:
+ assert self.both_add_as_friend_norequest()
+ else:
+ assert self.both_add_as_friend()
+
+ if not self.wait_friend_get_connection_status(self.bob, self.baid, n=iN):
+ LOG.warn('baid not connected')
+ if not self.wait_friend_get_connection_status(self.alice, self.abid, n=iN):
+ LOG.warn('abid not connected')
+
+ self.bob.callback_friend_read_receipt(their_on_read_reciept) #was their_on_friend_action
+ self.alice.callback_friend_read_receipt(their_on_read_reciept) #was their_on_friend_action
+ self.warn_if_no_cb(self.bob, 'friend_read_receipt')
+ self.warn_if_no_cb(self.alice, 'friend_read_receipt')
+ if True:
+ iMsg = self.bob.friend_send_message(self.baid,
+ TOX_MESSAGE_TYPE['ACTION'],
+ bytes(ACTION, 'UTF-8'))
+ assert iMsg >= 0
+ else:
+ assert self.wait_ensure_exec(self.bob.friend_send_message,
+ [self.baid,
+ TOX_MESSAGE_TYPE['ACTION'],
+ bytes(ACTION, 'UTF-8')])
+ if not self.wait_otox_attrs(self.alice, [sSlot]):
+ LOG_WARN(f"alice test_friend_action NO {sSlot}")
+ except AssertionError as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ except ArgumentError as e:
+ # ArgumentError('This client is currently NOT CONNECTED to the friend.')
+ # dunno
+ LOG.warning(f"test_friend_action {e}")
+ except Exception as e:
+ LOG.error(f"test_friend_action {e}")
+ raise
+ finally:
+ self.alice.callback_friend_read_receipt(None)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ def test_alice_typing_status(self) -> None: # works
+ """
+ t:on_friend_read_receipt
+ t:on_friend_typing
+ t:self_set_typing
+ t:friend_get_typing
+ t:friend_get_last_online
+ """
+
+ sSlot = 'friend_typing'
+ LOG.info("test_typing_status bob adding alice")
+ #: Test typing status
+ def bob_on_friend_typing(iTox, fid:int, is_typing, *largs) -> None:
+ LOG_INFO(f"BOB_ON_friend_typing is_typing={is_typing} fid={fid}")
+ try:
+ assert fid == self.baid
+ if is_typing is True:
+ assert self.bob.friend_get_typing(fid) is True
+ except Exception as e:
+ LOG_ERROR(f"BOB_ON_friend_typing {e}")
+ setattr(self.bob, sSlot, True)
+
+ setattr(self.bob, sSlot, None)
+ try:
+ if bUSE_NOREQUEST:
+ assert self.both_add_as_friend_norequest()
+ else:
+ assert self.both_add_as_friend()
+
+ if not self.get_connection_status():
+ LOG.warning(f"test_friend_typing NOT CONNECTED")
+ self.loop_until_connected()
+
+ self.bob.callback_friend_typing(bob_on_friend_typing)
+ self.warn_if_no_cb(self.bob, sSlot)
+ self.alice.self_set_typing(self.abid, False)
+ if not self.wait_otox_attrs(self.bob, [sSlot]):
+ LOG_WARN(f"bobs_on_friend_typing NO {sSlot}")
+ except AssertionError as e:
+ LOG.error(f"Failed test {e}")
+ raise
+ except Exception as e:
+ LOG.error(f"test_alice_typing_status error={e}")
+ raise
+ finally:
+ self.bob.callback_friend_typing(None)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ @expectedFail('fails') # @unittest.skip('unfinished')
+ def test_file_transfer(self) -> None: # unfinished
+ """
+ t:file_send
+ t:file_send_chunk
+ t:file_control
+ t:file_seek
+ t:file_get_file_id
+ t:on_file_recv
+ t:on_file_recv_control
+ t:on_file_recv_chunk
+ t:on_file_chunk_request
+ """
+
+ if bUSE_NOREQUEST:
+ assert self.both_add_as_friend_norequest()
+ else:
+ assert self.both_add_as_friend()
+
+ FRIEND_NUMBER = self.baid
+ FILE_NUMBER = 1
+ FILE = os.urandom(1024 * 1024)
+ FILE_NAME = b"/tmp/test.bin"
+ if not os.path.exists(FILE_NAME):
+ with open(FILE_NAME, 'wb') as oFd:
+ oFd.write(FILE)
+ FILE_SIZE = len(FILE)
+ OFFSET = 567
+ # was FILE_ID = FILE_NAME
+ FILE_ID = 32*'1' #
+
+ m = hashlib.md5()
+ m.update(FILE[OFFSET:])
+ FILE_DIGEST = m.hexdigest()
+
+ CONTEXT = { 'FILE': bytes(), 'RECEIVED': 0, 'START': False, 'SENT': 0 }
+
+ def alice_on_file_recv(iTox, fid:int, file_number:int, kind, size, filename) -> None:
+ LOG_DEBUG(f"ALICE_ON_file_recv fid={fid} {file_number}")
+ try:
+ assert size == FILE_SIZE
+ assert filename == FILE_NAME
+ retv = self.alice.file_seek(fid, file_number, OFFSET)
+ assert retv is True
+ self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['RESUME'])
+ except Exception as e:
+ LOG_ERROR(f"ALICE_ON_file_recv {e}")
+ else:
+ LOG_INFO(f"ALICE_ON_file_recv " + str(fid))
+
+ def alice_on_file_recv_control(iTox, fid:int, file_number, control, *largs) -> None:
+ # TOX_FILE_CONTROL = { 'RESUME': 0, 'PAUSE': 1, 'CANCEL': 2,}
+ LOG_DEBUG(f"ALICE_ON_file_recv_control fid={fid} {file_number} {control}")
+ try:
+ assert FILE_NUMBER == file_number
+ # FixMe _FINISHED?
+ if False and control == TOX_FILE_CONTROL['RESUME']:
+ # assert CONTEXT['RECEIVED'] == FILE_SIZE
+ # m = hashlib.md5()
+ # m.update(CONTEXT['FILE'])
+ # assert m.hexdigest() == FILE_DIGEST
+ self.alice.completed = True
+ except Exception as e:
+ LOG_ERROR(f"ALICE_ON_file_recv {e}")
+ else:
+ LOG_INFO(f"ALICE_ON_file_recv " + str(fid))
+
+ self.alice.completed = False
+ def alice_on_file_recv_chunk(iTox, fid:int, file_number:int, position:int, iNumBytes, *largs) -> bool:
+ LOG_DEBUG(f"ALICE_ON_file_recv_chunk {fid} {file_number}")
+ # FixMe - use file_number and iNumBytes to get data?
+ data = ''
+ LOG_INFO(f"ALICE_ON_file_recv_chunk {fid}")
+ try:
+ if data is None:
+ assert CONTEXT['RECEIVED'] == (FILE_SIZE - OFFSET)
+ m = hashlib.md5()
+ m.update(CONTEXT['FILE'])
+ assert m.hexdigest() == FILE_DIGEST
+ self.alice.completed = True
+ self.alice.file_control(fid, file_number, TOX_FILE_CONTROL['CANCEL'])
+ return True
+
+ CONTEXT['FILE'] += data
+ CONTEXT['RECEIVED'] += len(data)
+ # if CONTEXT['RECEIVED'] < FILE_SIZE:
+ # assert self.file_data_remaining(
+ # fid, file_number, 1) == FILE_SIZE - CONTEXT['RECEIVED']
+ except Exception as e:
+ LOG_ERROR(f"ALICE_ON_file_recv_chunk {e}")
+ return False
+ return True
+
+ # AliceTox.on_file_send_request = on_file_send_request
+ # AliceTox.on_file_control = on_file_control
+ # AliceTox.on_file_data = on_file_data
+
+ try:
+ # required
+ assert self.wait_friend_get_connection_status(self.bob, self.baid, n=2*iN)
+ assert self.wait_friend_get_connection_status(self.alice, self.abid, n=2*iN)
+
+ self.alice.callback_file_recv(alice_on_file_recv)
+ self.alice.callback_file_recv_control(alice_on_file_recv_control)
+ self.alice.callback_file_recv_chunk(alice_on_file_recv_chunk)
+
+ self.bob.completed = False
+ def bob_on_file_recv_control2(iTox, fid:int, file_number:int, control) -> None:
+ LOG_DEBUG(f"BOB_ON_file_recv_control2 {fid} {file_number} control={control}")
+ if control == TOX_FILE_CONTROL['RESUME']:
+ CONTEXT['START'] = True
+ elif control == TOX_FILE_CONTROL['CANCEL']:
+ self.bob.completed = True
+ pass
+
+ def bob_on_file_chunk_request(iTox, fid:int, file_number:int, position:int, length, *largs) -> None:
+ LOG_DEBUG(f"BOB_ON_file_chunk_request {fid} {file_number}")
+ if length == 0:
+ return
+ data = FILE[position:(position + length)]
+ self.bob.file_send_chunk(fid, file_number, position, data)
+
+ sSlot = 'file_recv_control'
+ self.bob.callback_file_recv_control(bob_on_file_recv_control2)
+ self.bob.callback_file_chunk_request(bob_on_file_chunk_request)
+
+ i = 0
+ iKind = 0
+ while i < 2:
+ i += 1
+ try:
+ FN = self.bob.file_send(self.baid, iKind, FILE_SIZE, FILE_ID, FILE_NAME)
+ LOG.info(f"test_file_transfer bob.file_send {FN}")
+ except ArgumentError as e:
+ LOG.debug(f"test_file_transfer bob.file_send {e} {i}")
+ # ctypes.ArgumentError: This client is currently not connected to the friend
+ raise
+ else:
+ break
+ self.loop(100)
+ sleep(1)
+ else:
+ LOG.error(f"test_file_transfer bob.file_send 2")
+ raise AssertionError(f"test_file_transfer bob.file_send {THRESHOLD // 2}")
+
+ # UINT32_MAX
+ try:
+ FID = self.bob.file_get_file_id(self.baid, FN)
+ hexFID = "".join([hex(ord(c))[2:].zfill(2) for c in FILE_NAME])
+ assert FID.startswith(hexFID.upper())
+ except Exception as e:
+ LOG.warn(f"test_file_transfer:: {FILE_NAME} {hexFID} {e}")
+ LOG.debug('\n' + traceback.format_exc())
+
+ if not self.wait_otox_attrs(self.bob, ['completed']):
+ LOG_WARN(f"test_file_transfer Bob NO completed")
+ return False
+ if not self.wait_otox_attrs(self.alice, ['completed']):
+ LOG_WARN(f"test_file_transfer Alice NO completed")
+ return False
+ return True
+
+ except (ArgumentError, ValueError,) as e:
+ # ValueError: non-hexadecimal number found in fromhex() arg at position 0
+ LOG.error(f"test_file_transfer: {e}")
+ raise
+
+ except Exception as e:
+ LOG.error(f"test_file_transfer:: {e}")
+ LOG.debug('\n' + traceback.format_exc())
+ raise
+
+ finally:
+ self.alice.callback_file_recv(None)
+ self.alice.callback_file_recv_control(None)
+ self.alice.callback_file_recv_chunk(None)
+ self.bob.callback_file_recv_control(None)
+ self.bob.callback_file_chunk_request(None)
+ if hasattr(self, 'baid') and self.baid >= 0:
+ self.bob.friend_delete(self.baid)
+ if hasattr(self, 'abid') and self.abid >= 0:
+ self.alice.friend_delete(self.abid)
+
+ LOG_INFO(f"test_file_transfer:: self.wait_objs_attr completed")
+
+ @unittest.skip('crashes')
+ def test_tox_savedata(self) -> None: #
+ """
+ t:get_savedata_size
+ t:get_savedata
+ """
+ # Fatal Python error: Aborted
+ # "/var/local/src/toxygen_wrapper/wrapper/tox.py", line 180 in kill
+
+ assert self.alice.get_savedata_size() > 0
+ data = self.alice.get_savedata()
+ assert data is not None
+ addr = self.alice.self_get_address()
+ # self._address
+
+ try:
+ LOG.info("test_tox_savedata alice.kill")
+ # crashes
+ self.alice.kill()
+ del self.alice
+ except:
+ pass
+
+ oArgs = oTOX_OARGS
+ opts = oToxygenToxOptions(oArgs)
+ opts.savedata_data = data
+ opts.savedata_length = len(data)
+
+ self.alice = Tox(tox_options=opts)
+ if addr != self.alice.self_get_address():
+ LOG.warning("test_tox_savedata " +
+ f"{addr} != {self.alice.self_get_address()}")
+ else:
+ LOG.info("passed test_tox_savedata")
+
+ def test_kill(self) -> None: #
+ import threading
+ LOG.info(f"THE END {threading.active_count()}")
+ self.tearDown()
+ LOG.info(f"THE END {threading.enumerate()}")
+
+
+def vOargsToxPreamble(oArgs, Tox, ToxTest) -> None:
+
+ ts.vSetupLogging(oArgs)
+
+ methods = set([x for x in dir(Tox) if not x[0].isupper()
+ and not x[0] == '_'])
+ docs = "".join([getattr(ToxTest, x).__doc__ for x in dir(ToxTest)
+ if getattr(ToxTest, x).__doc__ is not None])
+
+ tested = set(re.findall(r't:(.*?)\n', docs))
+ not_tested = methods.difference(tested)
+
+ logging.info('Test Coverage: %.2f%%' % (len(tested) * 100.0 / len(methods)))
+ if len(not_tested):
+ logging.info('Not tested:\n %s' % "\n ".join(sorted(list(not_tested))))
+
+
+###
+
+def iMain(oArgs, failfast=True) -> int:
+
+# collect_types.init_types_collection()
+
+ vOargsToxPreamble(oArgs, Tox, ToxSuite)
+ # https://stackoverflow.com/questions/35930811/how-to-sort-unittest-testcases-properly/35930812#35930812
+ cases = ts.suiteFactory(*ts.caseFactory([ToxSuite]))
+ if color_runner:
+ runner = color_runner.runner.TextTestRunner(verbosity=2, failfast=failfast)
+ else:
+ runner = unittest.TextTestRunner(verbosity=2, failfast=failfast, warnings='ignore')
+
+# with collect_types.collect():
+ runner.run(cases)
+ # collect_types.dump_stats('tests_wrapper.out')
+
+def oToxygenToxOptions(oArgs):
+ data = None
+ tox_options = tox_wrapper.tox.Tox.options_new()
+ if oArgs.proxy_type:
+ tox_options.contents.proxy_type = int(oArgs.proxy_type)
+ tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8')
+ tox_options.contents.proxy_port = int(oArgs.proxy_port)
+ tox_options.contents.udp_enabled = False
+ else:
+ tox_options.contents.udp_enabled = oArgs.udp_enabled
+ if not os.path.exists('/proc/sys/net/ipv6'):
+ oArgs.ipv6_enabled = False
+ else:
+ tox_options.contents.ipv6_enabled = oArgs.ipv6_enabled
+
+ tox_options.contents.tcp_port = int(oArgs.tcp_port)
+ tox_options.contents.dht_announcements_enabled = oArgs.dht_announcements_enabled
+ tox_options.contents.hole_punching_enabled = oArgs.hole_punching_enabled
+
+ # overrides
+ tox_options.contents.local_discovery_enabled = False
+ tox_options.contents.experimental_thread_safety = False
+ # REQUIRED!!
+ if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'):
+ LOG.warning('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled))
+ tox_options.contents.ipv6_enabled = False
+ else:
+ tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled)
+
+ if data: # load existing profile
+ tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE']
+ tox_options.contents.savedata_data = c_char_p(data)
+ tox_options.contents.savedata_length = len(data)
+ else: # create new profile
+ tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE']
+ tox_options.contents.savedata_data = None
+ tox_options.contents.savedata_length = 0
+
+ #? tox_options.contents.log_callback = LOG
+ if tox_options._options_pointer:
+ # LOG.debug("Adding logging to tox_options._options_pointer ")
+ ts.vAddLoggerCallback(tox_options, ts.on_log)
+ else:
+ LOG.warning("No tox_options._options_pointer " +repr(tox_options._options_pointer))
+
+ return tox_options
+
+def oArgparse(lArgv):
+ parser = ts.oMainArgparser()
+ parser.add_argument('--norequest',type=str, default='False',
+ choices=['True','False'],
+ help='Use _norequest')
+ parser.add_argument('profile', type=str, nargs='?', default=None,
+ help='Path to Tox profile')
+ oArgs = parser.parse_args(lArgv)
+
+ for key in ts.lBOOLEANS:
+ if key not in oArgs: continue
+ val = getattr(oArgs, key)
+ setattr(oArgs, key, bool(val))
+
+ if hasattr(oArgs, 'sleep'):
+ if oArgs.sleep == 'qt':
+ pass # broken or gevent.sleep(idle_period)
+ elif oArgs.sleep == 'gevent':
+ pass # broken or gevent.sleep(idle_period)
+ else:
+ oArgs.sleep = 'time'
+
+ return oArgs
+
+def main(lArgs=None) -> int:
+ global oTOX_OARGS
+ if lArgs is None: lArgs = []
+ oArgs = oArgparse(lArgs)
+ global bIS_LOCAL
+ bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local']
+ oTOX_OARGS = oArgs
+ setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL)
+ bIS_LOCAL = True
+ setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL)
+ # oTOX_OPTIONS = ToxOptions()
+ global oTOX_OPTIONS
+ oTOX_OPTIONS = oToxygenToxOptions(oArgs)
+ if coloredlogs:
+ # https://pypi.org/project/coloredlogs/
+ coloredlogs.install(level=oArgs.loglevel,
+ logger=LOG,
+ # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d]
+ fmt='%(name)s %(levelname)s %(message)s'
+ )
+ else:
+ logging.basicConfig(level=oArgs.loglevel) # logging.INFO
+
+ return iMain(oArgs)
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/tox_wrapper/tox.py b/tox_wrapper/tox.py
new file mode 100644
index 0000000..bebf358
--- /dev/null
+++ b/tox_wrapper/tox.py
@@ -0,0 +1,3374 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+# ctypes wrapping of libtoxcore
+
+# WIP: - all functions are being changed to accept strings or byres for variables
+# the library will use as bytes, and return sstrings not bytes for things
+# you will use as strings. YMMV.
+
+from ctypes import *
+from datetime import datetime
+from typing import Union, Callable
+
+try:
+ from tox_wrapper.libtox import LibToxCore
+ from tox_wrapper.toxav import ToxAV
+ from tox_wrapper.toxcore_enums_and_consts import *
+ import tox_wrapper.toxcore_enums_and_consts as enum
+except:
+ from libtox import LibToxCore
+ from toxav import ToxAV
+ from toxcore_enums_and_consts import *
+ import toxcore_enums_and_consts as enum
+
+# callbacks can be called in any thread so were being careful
+# tox.py can be called by callbacks
+def LOG_ERROR(a) -> None:
+ print('EROR> '+a)
+def LOG_WARN(a) -> None:
+ print('WARN> '+a)
+def LOG_INFO(a) -> None:
+ bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20
+ if bVERBOSE: print('INFO> '+a)
+def LOG_DEBUG(a) -> None:
+ bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10
+ if bVERBOSE: print('DBUG> '+a)
+def LOG_TRACE(a) -> None:
+ bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10
+ if bVERBOSE: print('TRAC> '+a)
+
+UINT32_MAX = 2 ** 32 -1
+class ToxError(RuntimeError): pass
+TOX_MAX_STATUS_MESSAGE_LENGTH = 1007
+
+global aTIMES
+aTIMES=dict()
+def bTooSoon(key, sSlot, fSec=10.0) -> bool:
+ # rate limiting
+ global aTIMES
+ if sSlot not in aTIMES:
+ aTIMES[sSlot] = dict()
+ OTIME = aTIMES[sSlot]
+ now = datetime.now()
+ if key not in OTIME:
+ OTIME[key] = now
+ return False
+ delta = now - OTIME[key]
+ OTIME[key] = now
+ if delta.total_seconds() < fSec: return True
+ return False
+
+
+class ToxOptions(Structure):
+ _fields_ = [
+ ('ipv6_enabled', c_bool),
+ ('udp_enabled', c_bool),
+ ('local_discovery_enabled', c_bool),
+ ('dht_announcements_enabled', c_bool),
+ ('proxy_type', c_int),
+ ('proxy_host', c_char_p),
+ ('proxy_port', c_uint16),
+ ('start_port', c_uint16),
+ ('end_port', c_uint16),
+ ('tcp_port', c_uint16),
+ ('hole_punching_enabled', c_bool),
+ ('savedata_type', c_int),
+ ('savedata_data', c_char_p),
+ ('savedata_length', c_size_t),
+ ('log_callback', c_void_p),
+ ('log_user_data', c_void_p),
+ ('experimental_thread_safety', c_bool),
+ ('operating_system', c_void_p),
+ ]
+
+
+class GroupChatSelfPeerInfo(Structure):
+ _fields_ = [
+ ('nick', c_char_p),
+ ('nick_length', c_uint8),
+ ('user_status', c_int)
+ ]
+
+def string_to_bin_charp(tox_id):
+ assert type(tox_id) == str, f"{type(tox_id)} != str"
+ return c_char_p(bytes.fromhex(tox_id)) if tox_id is not None else None
+
+
+def bin_to_string(raw_id, length) -> str:
+ res = ''.join('{:02x}'.format(ord(raw_id[i])) for i in range(length))
+ return res.upper()
+
+def sGetError(value, a) -> str:
+ # dict(enumerate(a))[value]
+ for k,v in a.items():
+ if v == value:
+ s = k
+ return s
+ return ''
+
+class Tox:
+ libtoxcore = LibToxCore()
+
+ def __init__(self, tox_options=None, tox_pointer=None, app=None):
+ """Creates and initialises a new Tox instance with the options passed.
+
+ This function will bring the instance into a valid state.
+ Running the event loop with a new instance will operate correctly.
+
+ :param tox_options: An options object. If this parameter is None, the default options are used.
+ :param tox_pointer: Tox instance pointer. If this parameter is not None, tox_options will be ignored.
+
+ """
+ self._app = app # QtWidgets.QApplication.instance()
+ if tox_pointer is not None:
+ self._tox_pointer = tox_pointer
+ else:
+ tox_err_new = c_int()
+ f = Tox.libtoxcore.tox_new
+ f.restype = POINTER(c_void_p)
+ self._tox_pointer = f(tox_options, byref(tox_err_new))
+ tox_err_new = tox_err_new.value
+ if tox_err_new == TOX_ERR_NEW['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_new == TOX_ERR_NEW['MALLOC']:
+ raise MemoryError('The function was unable to allocate enough '
+ 'memory to store the internal structures for the Tox object.')
+ if tox_err_new == TOX_ERR_NEW['PORT_ALLOC']:
+ raise ToxError('The function was unable to bind to a port. This may mean that all ports have '
+ 'already been bound, e.g. by other Tox instances, or it may mean a permission error.'
+ ' You may be able to gather more information from errno.')
+ if tox_err_new == TOX_ERR_NEW['TCP_SERVER_ALLOC']:
+ raise ToxError('The function was unable to bind the tcp server port.')
+ if tox_err_new == TOX_ERR_NEW['PROXY_BAD_TYPE']:
+ raise ArgumentError('proxy_type was invalid.')
+ if tox_err_new == TOX_ERR_NEW['PROXY_BAD_HOST']:
+ raise ArgumentError('proxy_type was valid but the proxy_host passed had an invalid format or was NULL.')
+ if tox_err_new == TOX_ERR_NEW['PROXY_BAD_PORT']:
+ raise ArgumentError('proxy_type was valid, but the proxy_port was invalid.')
+ if tox_err_new == TOX_ERR_NEW['PROXY_NOT_FOUND']:
+ raise ArgumentError('The proxy address passed could not be resolved.')
+ if tox_err_new == TOX_ERR_NEW['LOAD_ENCRYPTED']:
+ raise ArgumentError('The byte array to be loaded contained an encrypted save.')
+ if tox_err_new == TOX_ERR_NEW['LOAD_BAD_FORMAT']:
+ raise ArgumentError('The data format was invalid. This can happen when loading data that was saved by'
+ ' an older version of Tox, or when the data has been corrupted. When loading from'
+ ' badly formatted data, some data may have been loaded, and the rest is discarded.'
+ ' Passing an invalid length parameter also causes this error.')
+
+ self.self_connection_status_cb = None
+ self.self_logger_cb = None
+ self.friend_name_cb = None
+ self.friend_status_message_cb = None
+ self.friend_status_cb = None
+ self.friend_connection_status_cb = None
+ self.friend_request_cb = None
+ self.friend_read_receipt_cb = None
+ self.friend_typing_cb = None
+ self.friend_message_cb = None
+ self.file_recv_control_cb = None
+ self.file_chunk_request_cb = None
+ self.file_recv_cb = None
+ self.file_recv_chunk_cb = None
+ self.friend_lossy_packet_cb = None
+ self.friend_lossless_packet_cb = None
+ self.group_moderation_cb = None
+ self.group_join_fail_cb = None
+ self.group_self_join_cb = None
+ self.group_invite_cb = None
+ self.group_custom_packet_cb = None
+ self.group_private_message_cb = None
+ self.group_message_cb = None
+
+ self.group_password_cb = None
+ self.group_peer_limit_cb = None
+ self.group_privacy_state_cb = None
+ self.group_topic_cb = None
+ self.group_peer_status_cb = None
+ self.group_peer_name_cb = None
+ self.group_peer_exit_cb = None
+ self.group_peer_join_cb = None
+ self.AV = ToxAV(self._tox_pointer)
+
+ def kill(self) -> None:
+ if hasattr(self, 'AV'): del self.AV
+ LOG_INFO(f"tox.kill")
+ try:
+ Tox.libtoxcore.tox_kill(self._tox_pointer)
+ except Exception as e:
+ LOG_ERROR(f"tox.kill {e!s}")
+ else:
+ LOG_DEBUG(f"tox.kill")
+ return None
+
+ # Startup options
+
+ @staticmethod
+ def options_default(tox_options) -> None:
+ """Initialises a Tox_Options object with the default options.
+
+ The result of this function is independent of the original
+ options. All values will be overwritten, no values will be read
+ (so it is permissible to pass an uninitialised object).
+
+ If options is NULL, this function has no effect.
+
+ :param tox_options: A pointer to options object to be filled with default options.
+ return value: None
+ """
+ LOG_DEBUG(f"tox.options_default")
+ Tox.libtoxcore.tox_options_default(tox_options)
+ return None
+
+ @staticmethod
+ def options_new(): # a pointer
+ """Allocates a new Tox_Options object and initialises it with
+
+ the default options. This function can be used to preserve long
+ term ABI compatibility by giving the responsibility of
+ allocation and deallocation to the Tox library.
+
+ Objects returned from this function must be freed using the
+ tox_options_free function.
+
+ :return: A pointer to new ToxOptions object with default options or raise MemoryError.
+ """
+ tox_err_options_new = c_int()
+ f = Tox.libtoxcore.tox_options_new
+ f.restype = POINTER(ToxOptions)
+ result = f(byref(tox_err_options_new))
+ result._options_pointer = result
+ tox_err_options_new = tox_err_options_new.value
+ if tox_err_options_new == TOX_ERR_OPTIONS_NEW['OK']:
+ return result
+ if tox_err_options_new == TOX_ERR_OPTIONS_NEW['MALLOC']:
+ raise MemoryError('The function failed to allocate enough memory for the options struct.')
+ raise ToxError('The function did not return OK for the options struct.')
+
+ @staticmethod
+ def options_free(tox_options) -> None:
+ """
+ Releases all resources associated with an options objects.
+
+ Passing a pointer that was not returned by tox_options_new results in undefined behaviour.
+
+ :param tox_options: A pointer to new ToxOptions object
+ """
+ LOG_DEBUG(f"tox.options_free")
+ Tox.libtoxcore.tox_options_free(tox_options)
+
+ # Creation and destruction
+
+ def get_savedata_size(self) -> int:
+ """
+ Calculates the number of bytes required to store the tox instance with tox_get_savedata.
+ This function cannot fail. The result is always greater than 0.
+
+ :return: number of bytes
+ """
+ return int(Tox.libtoxcore.tox_get_savedata_size(self._tox_pointer))
+
+ def get_savedata(self, savedata: Union[Array, None]=None) -> bytes:
+ """
+ Store all information associated with the tox instance to a byte array.
+
+ :param savedata: pointer (c_char_p) to a memory region large enough to store the tox instance data.
+ Call tox_get_savedata_size to find the number of bytes required. If this parameter is None, this function
+ allocates memory for the tox instance data.
+ :return: pointer (c_char_p) to a memory region with the tox instance data
+ """
+ if savedata is None:
+ savedata_size = self.get_savedata_size()
+ savedata = create_string_buffer(savedata_size)
+ else:
+ isinstance(savedata, Array), type(savedata)
+ LOG_DEBUG(f"tox.get_savedata")
+ Tox.libtoxcore.tox_get_savedata(self._tox_pointer, savedata)
+ return bytes(savedata[:])
+
+ # Connection lifecycle and event loop
+
+ def bootstrap(self, address: Union[str,bytes], port: int, public_key: Union[bytes,str]) -> bool:
+ """Sends a "get nodes" request to the given bootstrap node with IP, port, and public key to setup connections.
+
+ This function will attempt to connect to the node using UDP.
+ You must use this function even if Tox_Options.udp_enabled was
+ set to false.
+
+ :param address: The hostname or IP address (IPv4 or IPv6) of the node.
+ :param port: The port on the host on which the bootstrap Tox instance is listening.
+ :param public_key: The long term public key of the bootstrap node (TOX_PUBLIC_KEY_SIZE bytes).
+ :return: True on success.
+
+ """
+ LOG_TRACE(f"tox_bootstrap={address}")
+ if type(address) == str:
+ address = bytes(address, 'utf-8')
+ if type(public_key) == bytes:
+ public_key = str(public_key, 'utf-8')
+ tox_err_bootstrap = c_int()
+ try:
+ result = Tox.libtoxcore.tox_bootstrap(self._tox_pointer,
+ c_char_p(address),
+ c_uint16(port),
+ string_to_bin_charp(public_key),
+ byref(tox_err_bootstrap))
+ except Exception as e:
+ # Fatal Python error: Segmentation fault
+ LOG_ERROR(f"libtoxcore.tox_bootstrap {e}")
+ # dunno
+ raise
+
+ tox_err_bootstrap = tox_err_bootstrap.value
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']:
+ return bool(result)
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']:
+ raise ArgumentError('The address could not be resolved to an IP '
+ 'address, or the address passed was invalid.')
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']:
+ raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).')
+ # me - this seems wrong - should be False
+ return False
+
+ def add_tcp_relay(self, address: Union[str,bytes], port: int, public_key: Union[bytes,str]) -> bool:
+ """Adds additional host:port pair as TCP relay.
+
+ This function can be used to initiate TCP connections to
+ different ports on the same bootstrap node, or to add TCP
+ relays without using them as bootstrap nodes.
+
+ :param address: The hostname or IP address (IPv4 or IPv6) of the TCP relay.
+ :param port: The port on the host on which the TCP relay is listening.
+ :param public_key: The long term public key of the TCP relay (TOX_PUBLIC_KEY_SIZE bytes).
+ :return: True on success.
+
+ """
+ LOG_TRACE(f"tox_add_tcp_relay address={address}")
+ if type(address) == str:
+ address = bytes(address, 'utf-8')
+ if type(public_key) == bytes:
+ public_key = str(public_key, 'utf-8')
+ tox_err_bootstrap = c_int()
+ result = Tox.libtoxcore.tox_add_tcp_relay(self._tox_pointer,
+ c_char_p(address),
+ c_uint16(port),
+ string_to_bin_charp(public_key),
+ byref(tox_err_bootstrap))
+ tox_err_bootstrap = tox_err_bootstrap.value
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['OK']:
+ return bool(result)
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_HOST']:
+ raise ArgumentError('The address could not be resolved to an IP '
+ 'address, or the IP address passed was invalid.')
+ if tox_err_bootstrap == TOX_ERR_BOOTSTRAP['BAD_PORT']:
+ raise ArgumentError('The port passed was invalid. The valid port range is (1, 65535).')
+ raise ToxError('The function did not return OK')
+
+ def self_get_connection_status(self) -> int:
+ """
+ Return whether we are connected to the DHT.
+ The return value is equal to the last value received through the
+ `self_connection_status` callback.
+
+ :return: TOX_CONNECTION
+ """
+ iRet = Tox.libtoxcore.tox_self_get_connection_status(self._tox_pointer)
+ if iRet > 2:
+ LOG_ERROR(f"self_get_connection_status {iRet} > 2")
+ return 0
+ LOG_TRACE(f"self_get_connection_status {iRet}")
+ return int(iRet)
+
+ def callback_self_connection_status(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `self_connection_status` event.
+ Pass None to unset.
+
+ This event is triggered whenever there is a change in the DHT
+ connection state. When disconnected, a client may choose to
+ call tox_bootstrap again, to reconnect to the DHT. Note that
+ this state may frequently change for short amounts of
+ time. Clients should therefore not immediately bootstrap on
+ receiving a disconnect.
+
+ :param callback: Python function. Should take
+ pointer (c_void_p) to Tox object,
+ TOX_CONNECTION (c_int),
+ pointer (c_void_p) to user_data
+
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_self_connection_status(self._tox_pointer,
+ POINTER(None)())
+ self.self_connection_status_cb = None
+ return
+
+ c_callback = CFUNCTYPE(None, c_void_p, c_int, c_void_p)
+ self.self_connection_status_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_self_connection_status")
+ Tox.libtoxcore.tox_callback_self_connection_status(self._tox_pointer,
+ self.self_connection_status_cb)
+
+ def iteration_interval(self) -> int:
+ """
+ Return the time in milliseconds before tox_iterate() should be
+ called again for optimal performance.
+ :return: time in milliseconds
+
+ """
+ return int(Tox.libtoxcore.tox_iteration_interval(self._tox_pointer))
+
+ def iterate(self, user_data: Union[bytes,None] = None) -> None: # void
+ """
+ The main loop that needs to be run in intervals of tox_iteration_interval() milliseconds.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ try:
+ LOG_TRACE(f"tox_iterate")
+ Tox.libtoxcore.tox_iterate(self._tox_pointer, c_char_p(user_data))
+ except Exception as e:
+ # Fatal Python error: Segmentation fault
+ LOG_ERROR(f"iterate {e!s}")
+ else:
+ LOG_TRACE(f"iterate")
+
+ # Internal client information (Tox address/id)
+
+ def self_get_toxid(self, address: Union[Array, None]=None) -> str:
+ return self.self_get_address(address)
+
+ def self_get_address(self, address: Union[Array, None]=None) -> str:
+ """
+ Writes the Tox friend address of the client to a byte array. The address is not in human-readable format. If a
+ client wants to display the address, formatting is required.
+
+ :param address: pointer (c_char_p) to a memory region of at least TOX_ADDRESS_SIZE bytes. If this parameter is
+ None, this function allocates memory for address.
+ :return: Tox friend address
+ """
+ if address is None:
+ address = create_string_buffer(TOX_ADDRESS_SIZE)
+ else:
+ isinstance(address, Array), type(address)
+ LOG_DEBUG(f"tox.self_get_address")
+ Tox.libtoxcore.tox_self_get_address(self._tox_pointer, address)
+ return bin_to_string(address, TOX_ADDRESS_SIZE)
+
+ def self_set_nospam(self, nospam: int) -> None:
+ """
+ Set the 4-byte nospam part of the address.
+
+ :param nospam: Any 32 bit unsigned integer.
+ """
+ LOG_DEBUG(f"tox.self_set_nospam")
+ Tox.libtoxcore.tox_self_set_nospam(self._tox_pointer, c_uint32(nospam))
+
+ def self_get_nospam(self) -> int:
+ """
+ Get the 4-byte nospam part of the address.
+
+ :return: nospam part of the address
+ """
+ return int(Tox.libtoxcore.tox_self_get_nospam(self._tox_pointer))
+
+ def self_get_public_key(self, public_key: Union[Array, None] = None) -> str:
+ """
+ Copy the Tox Public Key (long term) from the Tox object.
+
+ :param public_key: A memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is NULL, this
+ function allocates memory for Tox Public Key.
+ :return: Tox Public Key
+ """
+ if public_key is None:
+ public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE)
+ else:
+ isinstance(public_key, Array), type(public_key)
+ LOG_DEBUG(f"tox.self_get_public_key")
+ Tox.libtoxcore.tox_self_get_public_key(self._tox_pointer, public_key)
+ return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE)
+
+ def self_get_secret_key(self, secret_key: Union[Array, None] = None) -> str:
+ """
+ Copy the Tox Secret Key from the Tox object.
+
+ :param secret_key: pointer (c_char_p) to a memory region of at least TOX_SECRET_KEY_SIZE bytes. If this
+ parameter is NULL, this function allocates memory for Tox Secret Key.
+ :return: Tox Secret Key
+ """
+ if secret_key is None:
+ secret_key = create_string_buffer(TOX_SECRET_KEY_SIZE)
+ else:
+ isinstance(secret_key, Array), type(secret_key)
+ LOG_DEBUG(f"tox.self_get_secret_key")
+ Tox.libtoxcore.tox_self_get_secret_key(self._tox_pointer, secret_key)
+ return bin_to_string(secret_key, TOX_SECRET_KEY_SIZE)
+
+ # User-visible client information (nickname/status)
+
+ def self_set_name(self, name: Union[bytes,str]) -> bool:
+ """
+ Set the nickname for the Tox client.
+
+ Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length is 0, the name parameter is ignored
+ (it can be None), and the nickname is set back to empty.
+ :param name: New nickname.
+ :return: True on success.
+ """
+ tox_err_set_info = c_int()
+ if type(name) == str:
+ name = bytes(name, 'utf-8')
+ LOG_DEBUG(f"tox.self_set_name")
+ result = Tox.libtoxcore.tox_self_set_name(self._tox_pointer,
+ c_char_p(name),
+ c_size_t(len(name)),
+ byref(tox_err_set_info))
+ tox_err_set_info = tox_err_set_info.value
+ if tox_err_set_info == TOX_ERR_SET_INFO['OK']:
+ return bool(result)
+ elif tox_err_set_info == TOX_ERR_SET_INFO['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ elif tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']:
+ raise ArgumentError('Information length exceeded maximum permissible size.')
+ raise ToxError('The function did not return OK')
+
+ def self_get_name_size(self) -> int:
+ """
+ Return the length of the current nickname as passed to tox_self_set_name.
+
+ If no nickname was set before calling this function, the name is empty, and this function returns 0.
+
+ :return: length of the current nickname
+ """
+ retval = Tox.libtoxcore.tox_self_get_name_size(self._tox_pointer)
+ return int(retval)
+
+ def self_get_name(self, name: Union[Array,None] = None) -> str:
+ """
+ Write the nickname set by tox_self_set_name to a byte array.
+
+ If no nickname was set before calling this function, the name is empty, and this function has no effect.
+
+ Call tox_self_get_name_size to find out how much memory to allocate for the result.
+
+ :param name: pointer (c_char_p) to a memory region location large enough to hold the nickname. If this parameter
+ is NULL, the function allocates memory for the nickname.
+ :return: nickname
+ """
+ if name is None:
+ name = create_string_buffer(self.self_get_name_size())
+ else:
+ isinstance(name, Array), type(name)
+ LOG_DEBUG(f"tox.self_get_name")
+ Tox.libtoxcore.tox_self_get_name(self._tox_pointer, name)
+ return str(name.value, 'utf-8', errors='ignore')
+
+ def self_set_status_message(self, status_message: Union[bytes,str]) -> bool:
+ """Set the client's status message.
+
+ Status message length cannot exceed TOX_MAX_STATUS_MESSAGE_LENGTH.
+ If length is 0, the status parameter is ignored, and the user status is
+ set back to empty.
+
+ :param status_message: new status message
+ :return: True on success.
+ """
+ tox_err_set_info = c_int()
+ if len(status_message) > TOX_MAX_STATUS_MESSAGE_LENGTH:
+ status_message = status_message[:TOX_MAX_STATUS_MESSAGE_LENGTH]
+ if type(status_message) == str:
+ status_message = bytes(status_message, 'utf-8')
+ LOG_DEBUG(f"tox.self_set_status_message")
+ result = Tox.libtoxcore.tox_self_set_status_message(self._tox_pointer,
+ c_char_p(status_message),
+ c_size_t(len(status_message)),
+ byref(tox_err_set_info))
+ tox_err_set_info = tox_err_set_info.value
+ if tox_err_set_info == TOX_ERR_SET_INFO['OK']:
+ return bool(result)
+ if tox_err_set_info == TOX_ERR_SET_INFO['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_set_info == TOX_ERR_SET_INFO['TOO_LONG']:
+ raise ArgumentError('Information length exceeded maximum permissible size.')
+ raise ToxError('The function did not return OK.')
+
+ def self_get_status_message_size(self) -> int:
+ """
+ Return the length of the current status message as passed to tox_self_set_status_message.
+
+ If no status message was set before calling this function, the status is empty, and this function returns 0.
+
+ :return: length of the current status message
+ """
+ return Tox.libtoxcore.tox_self_get_status_message_size(self._tox_pointer)
+
+ def self_get_status_message(self, status_message: Union[Array,None] = None) -> str:
+ """
+ Write the status message set by tox_self_set_status_message to a byte array.
+
+ If no status message was set before calling this function, the status is empty, and this function has no effect.
+
+ Call tox_self_get_status_message_size to find out how much memory to allocate for the result.
+
+ :param status_message: pointer (c_char_p) to a valid memory location large enough to hold the status message.
+ If this parameter is None, the function allocates memory for the status message.
+ :return: status message
+ """
+ if status_message is None:
+ status_message = create_string_buffer(self.self_get_status_message_size())
+ else:
+ isinstance(status_message, Array), type(status_message)
+ LOG_DEBUG(f"tox.self_get_status_message")
+ Tox.libtoxcore.tox_self_get_status_message(self._tox_pointer, status_message)
+ return str(status_message.value, 'utf-8', errors='ignore')
+
+ def self_set_status(self, status: int) -> None:
+ """
+ Set the client's user status.
+
+ :param status: One of the user statuses listed in the enumeration TOX_USER_STATUS.
+ """
+ if bTooSoon('self', 'tox_self_set_status', 5.0): return None
+ LOG_DEBUG(f"tox.self_set_status {status}")
+ Tox.libtoxcore.tox_self_set_status(self._tox_pointer, c_uint32(status))
+ return None
+
+ def self_get_status(self) -> int:
+ """
+ Returns the client's user status.
+
+ :return: client's user status
+ """
+ LOG_TRACE(f"tox_get_status")
+ result = Tox.libtoxcore.tox_self_get_status(self._tox_pointer)
+ return int(result)
+
+ # Friend list management
+
+ def friend_add(self, address: Union[bytes,str], message: Union[bytes,str]) -> int:
+ """Add a friend to the friend list and send a friend request.
+
+ A friend request message must be at least 1 byte long and at
+ most TOX_MAX_FRIEND_REQUEST_LENGTH.
+
+ Friend numbers are unique identifiers used in all functions
+ that operate on friends. Once added, a friend number is stable
+ for the lifetime of the Tox object. After saving the state and
+ reloading it, the friend numbers may not be the same as
+ before. Deleting a friend creates a gap in the friend number
+ set, which is filled by the next adding of a friend. Any
+ pattern in friend numbers should not be relied on.
+
+ If more than INT32_MAX friends are added, this function causes
+ undefined behaviour.
+
+ :param address: The address of the friend (returned by tox_self_get_address of the friend you wish to add) it
+ must be TOX_ADDRESS_SIZE bytes.
+ :param message: The message that will be sent along with the friend request.
+ :return: the friend number on success, UINT32_MAX on failure.
+
+ """
+ tox_err_friend_add = c_int()
+ LOG_DEBUG(f"tox.friend_add")
+ if type(address) == bytes:
+ address = str(address, 'utf-8')
+ if type(message) == str:
+ message = bytes(message, 'utf-8')
+ result = Tox.libtoxcore.tox_friend_add(self._tox_pointer,
+ string_to_bin_charp(address),
+ c_char_p(message),
+ c_size_t(len(message)),
+ byref(tox_err_friend_add))
+ tox_err_friend_add = tox_err_friend_add.value
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']:
+ return int(result)
+
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']:
+ raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']:
+ raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be'
+ ' returned from tox_friend_add_norequest.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']:
+ raise ArgumentError('The friend address belongs to the sending client.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']:
+ raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is'
+ ' already on the friend list.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']:
+ raise ArgumentError('The friend address checksum failed.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']:
+ raise ArgumentError('The friend was already there, but the nospam value was different.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']:
+ raise MemoryError('A memory allocation failed when trying to increase the friend list size.')
+ raise ToxError('The function did not return OK for the friend add.')
+
+ def friend_add_norequest(self, public_key: Union[bytes,str]) -> int:
+ """Add a friend without sending a friend request.
+
+ This function is used to add a friend in response to a friend
+ request. If the client receives a friend request, it can be
+ reasonably sure that the other client added this client as a
+ friend, eliminating the need for a friend request.
+
+ This function is also useful in a situation where both
+ instances are controlled by the same entity, so that this
+ entity can perform the mutual friend adding. In this case,
+ there is no need for a friend request, either.
+
+ :param public_key: A byte array of length TOX_PUBLIC_KEY_SIZE containing the Public Key (not the Address) of the
+ friend to add.
+ :return: the friend number on success, UINT32_MAX on failure.
+
+ """
+ tox_err_friend_add = c_int()
+ LOG_DEBUG(f"tox.friend_add_norequest")
+ if type(public_key) == bytes:
+ public_key = str(public_key, 'utf-8')
+ result = Tox.libtoxcore.tox_friend_add_norequest(self._tox_pointer,
+ string_to_bin_charp(public_key),
+ byref(tox_err_friend_add))
+ tox_err_friend_add = tox_err_friend_add.value
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OK']:
+ return int(result)
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['TOO_LONG']:
+ raise ArgumentError('The length of the friend request message exceeded TOX_MAX_FRIEND_REQUEST_LENGTH.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['NO_MESSAGE']:
+ raise ArgumentError('The friend request message was empty. This, and the TOO_LONG code will never be'
+ ' returned from tox_friend_add_norequest.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['OWN_KEY']:
+ raise ArgumentError('The friend address belongs to the sending client.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['ALREADY_SENT']:
+ raise ArgumentError('A friend request has already been sent, or the address belongs to a friend that is'
+ ' already on the friend list.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['BAD_CHECKSUM']:
+ raise ArgumentError('The friend address checksum failed.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['SET_NEW_NOSPAM']:
+ raise ArgumentError('The friend was already there, but the nospam value was different.')
+ if tox_err_friend_add == TOX_ERR_FRIEND_ADD['MALLOC']:
+ raise MemoryError('A memory allocation failed when trying to increase the friend list size.')
+ raise ToxError('The function did not return OK for the friend add.')
+
+ def friend_delete(self, friend_number: int) -> bool:
+ """
+ Remove a friend from the friend list.
+
+ This does not notify the friend of their deletion. After calling this function, this client will appear offline
+ to the friend and no communication can occur between the two.
+
+ :param friend_number: Friend number for the friend to be deleted.
+ :return: True on success.
+ """
+ tox_err_friend_delete = c_int()
+ LOG_DEBUG(f"tox.friend_delete")
+ result = Tox.libtoxcore.tox_friend_delete(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_delete))
+ tox_err_friend_delete = tox_err_friend_delete.value
+ if tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['OK']:
+ return bool(result)
+ elif tox_err_friend_delete == TOX_ERR_FRIEND_DELETE['FRIEND_NOT_FOUND']:
+ raise ArgumentError('There was no friend with the given friend number. No friends were deleted.')
+ raise ToxError('The function did not return OK for the friend add.')
+
+ # Friend list queries
+
+ def friend_by_public_key(self, public_key: Union[str,bytes]) -> int:
+ """
+ Return the friend number associated with that Public Key.
+
+ :param public_key: A byte array containing the Public Key.
+ :return: friend number
+ """
+ tox_err_friend_by_public_key = c_int()
+ LOG_DEBUG(f"tox.friend_by_public_key")
+ if type(public_key) == bytes:
+ public_key = str(public_key, 'utf-8')
+ result = Tox.libtoxcore.tox_friend_by_public_key(self._tox_pointer,
+ string_to_bin_charp(public_key),
+ byref(tox_err_friend_by_public_key))
+ tox_err_friend_by_public_key = tox_err_friend_by_public_key.value
+ if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['OK']:
+ return int(result)
+ if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_by_public_key == TOX_ERR_FRIEND_BY_PUBLIC_KEY['NOT_FOUND']:
+ raise ArgumentError('No friend with the given Public Key exists on the friend list.')
+ raise ToxError('The function did not return OK for the friend by public key.')
+
+ def friend_exists(self, friend_number: int) -> bool:
+ """
+ Checks if a friend with the given friend number exists and returns true if it does.
+ """
+ assert type(friend_number) == int
+ # bool() -> TypeError: 'str' object cannot be interpreted as an integer
+ return bool(Tox.libtoxcore.tox_friend_exists(self._tox_pointer, c_uint32(friend_number)))
+
+ def self_get_friend_list_size(self) -> int:
+ """
+ Return the number of friends on the friend list.
+
+ This function can be used to determine how much memory to allocate for tox_self_get_friend_list.
+
+ :return: number of friends
+ """
+ return Tox.libtoxcore.tox_self_get_friend_list_size(self._tox_pointer)
+
+ def self_get_friend_list(self, friend_list: Union[list[int],None]=None) -> list:
+ """
+ Copy a list of valid friend numbers into an array.
+
+ Call tox_self_get_friend_list_size to determine the number of elements to allocate.
+
+ :param friend_list: pointer (c_char_p) to a memory region with enough space to hold the friend list. If this
+ parameter is None, this function allocates memory for the friend list.
+ :return: friend list
+ """
+ friend_list_size = self.self_get_friend_list_size()
+ if friend_list is None:
+ friend_list = create_string_buffer(sizeof(c_uint32) * friend_list_size)
+ friend_list = POINTER(c_uint32)(friend_list)
+ else:
+ isinstance(friend_list_size, Array), type(friend_list_size)
+ LOG_TRACE(f"tox_self_get_friend_list")
+ Tox.libtoxcore.tox_self_get_friend_list(self._tox_pointer, friend_list)
+ return friend_list[0:friend_list_size]
+
+ def friend_get_public_key(self, friend_number: int, public_key: Union[Array,None] = None) -> str:
+ """
+ Copies the Public Key associated with a given friend number to a byte array.
+
+ :param friend_number: The friend number you want the Public Key of.
+ :param public_key: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this
+ parameter is None, this function allocates memory for Tox Public Key.
+ :return: Tox Public Key
+ """
+ if public_key is None:
+ public_key = create_string_buffer(TOX_PUBLIC_KEY_SIZE)
+ else:
+ isinstance(public_key, Array), type(public_key)
+ tox_err_friend_get_public_key = c_int()
+ LOG_TRACE(f"tox_friend_get_public_key")
+ Tox.libtoxcore.tox_friend_get_public_key(self._tox_pointer,
+ c_uint32(friend_number),
+ public_key,
+ byref(tox_err_friend_get_public_key))
+ tox_err_friend_get_public_key = tox_err_friend_get_public_key.value
+ if tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['OK']:
+ return bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE)
+ elif tox_err_friend_get_public_key == TOX_ERR_FRIEND_GET_PUBLIC_KEY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('No friend with the given number exists on the friend list.')
+ raise ToxError('The function did not return OK')
+
+ def friend_get_last_online(self, friend_number: int) -> int:
+ """
+ Return a unix-time timestamp of the last time the friend associated with a given friend number was seen online.
+ This function will return UINT64_MAX on error.
+
+ :param friend_number: The friend number you want to query.
+ :return: unix-time timestamp
+ """
+ tox_err_last_online = c_int()
+ LOG_DEBUG(f"tox.friend_get_last_online")
+ result = Tox.libtoxcore.tox_friend_get_last_online(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_last_online))
+ tox_err_last_online = tox_err_last_online.value
+ if tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['OK']:
+ return int(result)
+ elif tox_err_last_online == TOX_ERR_FRIEND_GET_LAST_ONLINE['FRIEND_NOT_FOUND']:
+ raise ArgumentError('No friend with the given number exists on the friend list.')
+ raise ToxError('The function did not return OK')
+
+ # Friend-specific state queries (can also be received through callbacks)
+
+ def friend_get_name_size(self, friend_number: int) -> int:
+ """
+ Return the length of the friend's name. If the friend number is invalid, the return value is unspecified.
+
+ The return value is equal to the `length` argument received by the last `friend_name` callback.
+ """
+ tox_err_friend_query = c_int()
+ LOG_TRACE(f"tox_friend_get_name_size")
+ result = Tox.libtoxcore.tox_friend_get_name_size(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return int(result)
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK')
+
+ def friend_get_name(self, friend_number: int, name=None) -> str:
+ """Write the name of the friend designated by the given friend number to a byte array.
+
+ Call tox_friend_get_name_size to determine the allocation size
+ for the `name` parameter.
+
+ The data written to `name` is equal to the data received by the
+ last `friend_name` callback.
+
+ :param friend_number: number of friend
+ :param name: pointer (c_char_p) to a valid memory region large enough to store the friend's name.
+ :return: name of the friend
+ """
+ if name is None:
+ name = create_string_buffer(self.friend_get_name_size(friend_number))
+ else:
+ isinstance(name, Array), type(name)
+ tox_err_friend_query = c_int()
+ LOG_DEBUG(f"tox.friend_get_name")
+ Tox.libtoxcore.tox_friend_get_name(self._tox_pointer,
+ c_uint32(friend_number),
+ name,
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return str(name.value, 'utf-8', errors='ignore')
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK')
+
+ def callback_friend_name(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_name` event. Pass None to unset.
+
+ This event is triggered when a friend changes their name.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend whose name changed,
+ A byte array (c_char_p) containing the same data as tox_friend_get_name would write to its `name` parameter,
+ A value (c_size_t) equal to the return value of tox_friend_get_name_size,
+ pointer (c_void_p) to user_data
+ """
+ LOG_DEBUG(f"tox.callback_friend_name")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_name(self._tox_pointer,
+ POINTER(None)())
+ self.friend_name_cb = None
+ return
+
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p)
+ self.friend_name_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_name")
+ Tox.libtoxcore.tox_callback_friend_name(self._tox_pointer, self.friend_name_cb)
+
+ def friend_get_status_message_size(self, friend_number: int) -> int:
+ """
+ Return the length of the friend's status message. If the friend number is invalid, the return value is SIZE_MAX.
+
+ :return: length of the friend's status message
+ """
+ tox_err_friend_query = c_int()
+ LOG_TRACE(f"tox_friend_get_status_message_size")
+ result = Tox.libtoxcore.tox_friend_get_status_message_size(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return int(result)
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK')
+
+ def friend_get_status_message(self, friend_number: int, status_message=None) -> str:
+ """Write the status message of the friend designated by the given friend number to a byte array.
+
+ Call tox_friend_get_status_message_size to determine the
+ allocation size for the `status_name` parameter.
+
+ The data written to `status_message` is equal to the data
+ received by the last `friend_status_message` callback.
+
+ :param friend_number:
+ :param status_message: pointer (c_char_p) to a valid memory region large enough to store the friend's status
+ message.
+ :return: status message of the friend
+ """
+ if status_message is None:
+ status_message = create_string_buffer(self.friend_get_status_message_size(friend_number))
+ else:
+ isinstance(status_message, Array), type(status_message)
+ tox_err_friend_query = c_int()
+ LOG_DEBUG(f"tox.friend_get_status_message")
+ Tox.libtoxcore.tox_friend_get_status_message(self._tox_pointer,
+ c_uint32(friend_number),
+ status_message,
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ # 'utf-8' codec can't decode byte 0xb7 in position 2: invalid start byte
+ return str(status_message.value, 'utf-8', errors='ignore')
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK')
+
+ def callback_friend_status_message(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_status_message` event. Pass NULL to unset.
+
+ This event is triggered when a friend changes their status message.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend whose status message changed,
+ A byte array (c_char_p) containing the same data as tox_friend_get_status_message would write to its
+ `status_message` parameter,
+ A value (c_size_t) equal to the return value of tox_friend_get_status_message_size,
+ pointer (c_void_p) to user_data
+ """
+ LOG_DEBUG(f"tox.callback_friend_status_message")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_status_message(self._tox_pointer,
+ POINTER(None)())
+ self.friend_status_message_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p)
+ self.friend_status_message_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_status_message")
+ Tox.libtoxcore.tox_callback_friend_status_message(self._tox_pointer,
+ self.friend_status_message_cb)
+
+ def friend_get_status(self, friend_number: int) -> int:
+ """
+ Return the friend's user status (away/busy/...). If the friend number is invalid, the return value is
+ unspecified.
+
+ The status returned is equal to the last status received through the `friend_status` callback.
+
+ :return: TOX_USER_STATUS
+ """
+ tox_err_friend_query = c_int()
+ LOG_DEBUG(f"tox.friend_get_status")
+ result = Tox.libtoxcore.tox_friend_get_status(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return int(result)
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK.')
+
+ def callback_friend_status(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_status` event. Pass None to unset.
+
+ This event is triggered when a friend changes their user status.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ :param The friend number (c_uint32) of the friend whose user status changed,
+ :param The new user status (TOX_USER_STATUS),
+ :param user_data: pointer (c_void_p) to user data
+ """
+ LOG_DEBUG(f"tox.callback_friend_status")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer,
+ POINTER(None)())
+ self.friend_status_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p)
+ self.friend_status_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_status")
+ Tox.libtoxcore.tox_callback_friend_status(self._tox_pointer, self.friend_status_cb)
+ return None
+
+ def friend_get_connection_status(self, friend_number: int) -> int:
+ """
+ Check whether a friend is currently connected to this client.
+
+ The result of this function is equal to the last value received by the `friend_connection_status` callback.
+
+ :param friend_number: The friend number for which to query the connection status.
+ :return: the friend's connection status (TOX_CONNECTION) as it was received through the
+ `friend_connection_status` event.
+ """
+ tox_err_friend_query = c_int()
+ LOG_DEBUG(f"tox.friend_get_connection_status")
+ result = Tox.libtoxcore.tox_friend_get_connection_status(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return int(result)
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK for friend get connection status.')
+
+ def callback_friend_connection_status(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `friend_connection_status` event. Pass NULL to unset.
+
+ This event is triggered when a friend goes offline after having been online, or when a friend goes online.
+
+ This callback is not called when adding friends. It is assumed
+ that when adding friends, their connection status is initially
+ offline.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend whose connection status changed,
+ The result of calling tox_friend_get_connection_status (TOX_CONNECTION) on the passed friend_number,
+ pointer (c_void_p) to user_data
+ """
+ LOG_DEBUG(f"tox.callback_friend_connection_status")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_connection_status(self._tox_pointer,
+ POINTER(None)())
+ self.friend_connection_status_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p)
+ self.friend_connection_status_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_connection_status")
+ Tox.libtoxcore.tox_callback_friend_connection_status(self._tox_pointer,
+ self.friend_connection_status_cb)
+ return None
+
+ def friend_get_typing(self, friend_number: int) -> bool:
+ """
+ Check whether a friend is currently typing a message.
+
+ :param friend_number: The friend number for which to query the typing status.
+ :return: true if the friend is typing.
+ """
+ tox_err_friend_query = c_int()
+ LOG_DEBUG(f"tox.friend_get_typing")
+ result = Tox.libtoxcore.tox_friend_get_typing(self._tox_pointer,
+ c_uint32(friend_number),
+ byref(tox_err_friend_query))
+ tox_err_friend_query = tox_err_friend_query.value
+ if tox_err_friend_query == TOX_ERR_FRIEND_QUERY['OK']:
+ return bool(result)
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['NULL']:
+ raise ArgumentError('The pointer parameter for storing the query result (name, message) was NULL. Unlike'
+ ' the `_self_` variants of these functions, which have no effect when a parameter is'
+ ' NULL, these functions return an error in that case.')
+ elif tox_err_friend_query == TOX_ERR_FRIEND_QUERY['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number did not designate a valid friend.')
+ raise ToxError('The function did not return OK')
+
+ def callback_friend_typing(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_typing` event. Pass NULL to unset.
+
+ This event is triggered when a friend starts or stops typing.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend who started or stopped typing,
+ The result of calling tox_friend_get_typing (c_bool) on the passed friend_number,
+ pointer (c_void_p) to user_data
+ """
+ LOG_DEBUG(f"tox.callback_friend_typing")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_typing(self._tox_pointer,
+ POINTER(None)())
+ self.friend_typing_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_void_p)
+ self.friend_typing_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_typing")
+ Tox.libtoxcore.tox_callback_friend_typing(self._tox_pointer, self.friend_typing_cb)
+
+ # Sending private messages
+
+ def self_set_typing(self, friend_number: int, typing: bool) -> bool:
+ """
+ Set the client's typing status for a friend.
+
+ The client is responsible for turning it on or off.
+
+ :param friend_number: The friend to which the client is typing a message.
+ :param typing: The typing status. True means the client is typing.
+ :return: True on success.
+ """
+ tox_err_set_typing = c_int()
+ LOG_DEBUG(f"tox.self_set_typing")
+ result = Tox.libtoxcore.tox_self_set_typing(self._tox_pointer, c_uint32(friend_number),
+ c_bool(typing), byref(tox_err_set_typing))
+ tox_err_set_typing = tox_err_set_typing.value
+ if tox_err_set_typing == TOX_ERR_SET_TYPING['OK']:
+ return bool(result)
+ if tox_err_set_typing == TOX_ERR_SET_TYPING['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ raise ToxError('The function did not return OK for set typing.')
+
+ def friend_send_message(self, friend_number: int, message_type: int, message: Union[str,bytes]) -> int:
+ """Send a text chat message to an online friend.
+
+ This function creates a chat message packet and pushes it into the send queue.
+
+ The message length may not exceed
+ TOX_MAX_MESSAGE_LENGTH. Larger messages must be split by the
+ client and sent as separate messages. Other clients can then
+ reassemble the fragments. Messages may not be empty.
+
+ The return value of this function is the message ID. If a read
+ receipt is received, the triggered `friend_read_receipt` event
+ will be passed this message ID.
+
+ Message IDs are unique per friend. The first message ID is 0.
+ Message IDs are incremented by 1 each time a message is sent.
+ If UINT32_MAX messages were sent, the next message ID is 0.
+
+ :param friend_number: The friend number of the friend to send the message to.
+ :param message_type: Message type (TOX_MESSAGE_TYPE).
+ :param message: A non-None message text.
+ :return: message ID
+
+ """
+ if message and type(message) == str:
+ message = bytes(message, 'utf-8')
+ tox_err_friend_send_message = c_int()
+ LOG_DEBUG(f"tox.friend_send_message")
+ result = Tox.libtoxcore.tox_friend_send_message(self._tox_pointer,
+ c_uint32(friend_number),
+ c_int(message_type),
+ c_char_p(message), c_size_t(len(message)),
+ byref(tox_err_friend_send_message))
+ tox_err_friend_send_message = tox_err_friend_send_message.value
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['OK']:
+ return int(result)
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['SENDQ']:
+ raise MemoryError('An allocation error occurred while increasing the send queue size.')
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['TOO_LONG']:
+ raise ArgumentError('Message length exceeded TOX_MAX_MESSAGE_LENGTH.')
+ if tox_err_friend_send_message == TOX_ERR_FRIEND_SEND_MESSAGE['EMPTY']:
+ raise ArgumentError('Attempted to send a zero-length message.')
+ raise ToxError('The function did not return OK for friend send message.')
+
+ def callback_friend_read_receipt(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_read_receipt` event. Pass None to unset.
+
+ This event is triggered when the friend receives the message sent with tox_friend_send_message with the
+ corresponding message ID.
+
+ :param callback: Python function. Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend who received the message,
+ The message ID (c_uint32) as returned from tox_friend_send_message corresponding to the message sent,
+ pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ LOG_DEBUG(f"tox.callback_friend_read_receipt")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_read_receipt(self._tox_pointer,
+ POINTER(None)())
+ self.friend_read_receipt_cb = None
+ return
+
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p)
+ self.friend_read_receipt_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_read_receipt")
+ Tox.libtoxcore.tox_callback_friend_read_receipt(self._tox_pointer,
+ self.friend_read_receipt_cb)
+
+ # Receiving private messages and friend requests
+
+ def callback_friend_request(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_request` event. Pass None to unset.
+
+ This event is triggered when a friend request is received.
+
+ :param callback: Python function. Should take
+ pointer (c_void_p) to Tox object,
+ The Public Key (c_uint8 array) of the user who sent the friend request,
+ The message (c_char_p) they sent along with the request,
+ The size (c_size_t) of the message byte array,
+ pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_request(self._tox_pointer,
+ POINTER(None)())
+ self.friend_request_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, POINTER(c_uint8), c_char_p, c_size_t, c_void_p)
+ self.friend_request_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_request")
+ Tox.libtoxcore.tox_callback_friend_request(self._tox_pointer, self.friend_request_cb)
+
+ def callback_friend_message(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_message` event. Pass None to unset.
+
+ This event is triggered when a message from a friend is received.
+
+ :param callback: Python function. Should take
+ pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend who sent the message,
+ Message type (TOX_MESSAGE_TYPE),
+ The message data (c_char_p) they sent,
+ The size (c_size_t) of the message byte array.
+ pointer (c_void_p) to user_data
+ """
+ LOG_DEBUG(f"tox.callback_friend_message")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_friend_message(self._tox_pointer,
+ POINTER(None)())
+ self.friend_message_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_char_p, c_size_t, c_void_p)
+ self.friend_message_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_friend_message")
+ Tox.libtoxcore.tox_callback_friend_message(self._tox_pointer, self.friend_message_cb)
+
+ # File transmission: common between sending and receiving
+
+ @staticmethod
+ def hash(data: bytes, hash=None) -> str:
+ """Generates a cryptographic hash of the given data.
+
+ This function may be used by clients for any purpose, but is
+ provided primarily for validating cached avatars. This use is
+ highly recommended to avoid unnecessary avatar updates.
+
+ If hash is NULL or data is NULL while length is not 0 the function returns false, otherwise it returns true.
+
+ This function is a tox_wrapper to internal message-digest functions.
+
+ :param hash: A valid memory location the hash data. It must be at least TOX_HASH_LENGTH bytes in size.
+ :param data: Data to be hashed or NULL.
+#? :return: true if hash was not NULL.
+ :return: the hash as a string.
+ """
+ if hash is None:
+ hash = create_string_buffer(TOX_HASH_LENGTH)
+ else:
+ assert isinstance(hash, Array), f"{type(hash)}"
+ assert type(data) == bytes, f"{type(data)} != bytes"
+ Tox.libtoxcore.tox_hash(hash, c_char_p(data), c_size_t(len(data)))
+ return bin_to_string(hash, TOX_HASH_LENGTH)
+
+ def file_control(self, friend_number: int, file_number: int, control: int) -> bool:
+ """
+ Sends a file control command to a friend for a given file transfer.
+
+ :param friend_number: The friend number of the friend the file is being transferred to or received from.
+ :param file_number: The friend-specific identifier for the file transfer.
+ :param control: The control (TOX_FILE_CONTROL) command to send.
+ :return: True on success.
+ """
+ tox_err_file_control = c_int()
+ LOG_DEBUG(f"tox.file_control")
+ result = Tox.libtoxcore.tox_file_control(self._tox_pointer,
+ c_uint32(friend_number),
+ c_uint32(file_number),
+ c_int(control), byref(tox_err_file_control))
+ tox_err_file_control = tox_err_file_control.value
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['OK']:
+ return bool(result)
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if 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.')
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['NOT_PAUSED']:
+ raise ToxError('A RESUME control was sent, but the file transfer is running normally.')
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['DENIED']:
+ 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.')
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['ALREADY_PAUSED']:
+ raise ToxError('A PAUSE control was sent, but the file transfer was already paused.')
+ if tox_err_file_control == TOX_ERR_FILE_CONTROL['SENDQ']:
+ raise ToxError('Packet queue is full.')
+ raise ToxError('The function did not return OK for file control.')
+
+ def callback_file_recv_control(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `file_recv_control` event. Pass NULL to unset.
+
+ This event is triggered when a file control command is received
+ from a friend.
+
+ :param callback: Python function.
+ When receiving TOX_FILE_CONTROL_CANCEL, the client should
+ release the resources associated with the file number and
+ consider the transfer failed.
+
+ Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend who is sending the file.
+ The friend-specific file number (c_uint32) the data received is associated with.
+ The file control (TOX_FILE_CONTROL) command received.
+ pointer (c_void_p) to user_data
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_file_recv_control(self._tox_pointer,
+ POINTER(None)())
+ self.file_recv_control_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_file_recv_control")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p)
+ self.file_recv_control_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_file_recv_control")
+ Tox.libtoxcore.tox_callback_file_recv_control(self._tox_pointer,
+ self.file_recv_control_cb)
+
+ def file_seek(self, friend_number: int, file_number: int, position: int) -> bool:
+ """
+ Sends a file seek control command to a friend for a given file transfer.
+
+ This function can only be called to resume a file transfer right before TOX_FILE_CONTROL_RESUME is sent.
+
+ :param friend_number: The friend number of the friend the file is being received from.
+ :param file_number: The friend-specific identifier for the file transfer.
+ :param position: The position that the file should be seeked to.
+ :return: True on success.
+ """
+ tox_err_file_seek = c_int()
+ LOG_DEBUG(f"tox.file_control")
+ result = Tox.libtoxcore.tox_file_control(self._tox_pointer,
+ c_uint32(friend_number),
+ c_uint32(file_number),
+ c_uint64(position),
+ byref(tox_err_file_seek))
+ tox_err_file_seek = tox_err_file_seek.value
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['OK']:
+ return bool(result)
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['NOT_FOUND']:
+ raise ArgumentError('No file transfer with the given file number was found for the given friend.')
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['SEEK_DENIED']:
+ raise IOError('File was not in a state where it could be seeked.')
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['INVALID_POSITION']:
+ raise ArgumentError('Seek position was invalid')
+ if tox_err_file_seek == TOX_ERR_FILE_SEEK['SENDQ']:
+ raise ToxError('Packet queue is full.')
+ raise ToxError('The function did not return OK')
+
+ def file_get_file_id(self, friend_number: int, file_number: int, file_id=None) -> str:
+ """
+ Copy the file id associated to the file transfer to a byte array.
+
+ :param friend_number: The friend number of the friend the file is being transferred to or received from.
+ :param file_number: The friend-specific identifier for the file transfer.
+ :param file_id: A pointer (c_char_p) to memory region of at least TOX_FILE_ID_LENGTH bytes. If this parameter is
+ None, this function has no effect.
+ :return: file id.
+ """
+ if file_id is None:
+ file_id = create_string_buffer(TOX_FILE_ID_LENGTH)
+ else:
+ isinstance(file_id, Array), type(file_id)
+ tox_err_file_get = c_int()
+ LOG_DEBUG(f"tox.file_get_file_id")
+ Tox.libtoxcore.tox_file_get_file_id(self._tox_pointer,
+ c_uint32(friend_number),
+ c_uint32(file_number),
+ file_id,
+ byref(tox_err_file_get))
+ error = tox_err_file_get
+ if error.value == TOX_ERR_FILE_GET['OK']:
+ return bin_to_string(file_id, TOX_FILE_ID_LENGTH)
+ s = sGetError(error.value, TOX_ERR_FILE_GET)
+ LOG_ERROR(f"group_new err={error.value} {s}")
+ # have seen ArgumentError: group_new 3 NOT_FOUND
+ raise ArgumentError(f"group_new err={error.value} {s}")
+
+ # File transmission: sending
+
+ def file_send(self, friend_number: int, kind: int, file_size: int, file_id, filename: str) -> int:
+ """Send a file transmission request.
+
+ Maximum filename length is TOX_MAX_FILENAME_LENGTH bytes. The
+ filename should generally just be a file name, not a path with
+ directory names.
+
+ If a non-UINT64_MAX file size is provided, it can be used by
+ both sides to determine the sending progress. File size can be
+ set to UINT64_MAX for streaming data of unknown size.
+
+ File transmission occurs in chunks, which are requested
+ through the `file_chunk_request` event.
+
+ When a friend goes offline, all file transfers associated with
+ the friend are purged from core.
+
+ If the file contents change during a transfer, the behaviour
+ is unspecified in general. What will actually happen depends
+ on the mode in which the file was modified and how the client
+ determines the file size.
+
+ - If the file size was increased
+ - and sending mode was streaming (file_size = UINT64_MAX), the behaviour will be as expected.
+ - and sending mode was file (file_size != UINT64_MAX), the file_chunk_request callback will receive length =
+ 0 when Core thinks the file transfer has finished. If the client remembers the file size as it was when
+ sending the request, it will terminate the transfer normally. If the client re-reads the size, it will think
+ the friend cancelled the transfer.
+ - If the file size was decreased
+ - and sending mode was streaming, the behaviour is as expected.
+ - and sending mode was file, the callback will return 0 at the new (earlier) end-of-file, signalling to the
+ friend that the transfer was cancelled.
+ - If the file contents were modified
+ - at a position before the current read, the two files (local and remote) will differ after the transfer
+ terminates.
+ - at a position after the current read, the file transfer will succeed as expected.
+ - In either case, both sides will regard the transfer as complete and successful.
+
+ :param friend_number: The friend number of the friend the file send request should be sent to.
+ :param kind: The meaning of the file to be sent.
+ :param file_size: Size in bytes of the file the client wants to send, UINT64_MAX if unknown or streaming.
+ :param file_id: A file identifier of length TOX_FILE_ID_LENGTH that can be used to uniquely identify file
+ transfers across core restarts. If NULL, a random one will be generated by core. It can then be obtained by
+ using tox_file_get_file_id().
+ :param filename: Name of the file. Does not need to be the actual name. This name will be sent along with the
+ file send request.
+ :return: A file number used as an identifier in subsequent callbacks. This number is per friend. File numbers
+ are reused after a transfer terminates. On failure, this function returns UINT32_MAX. Any pattern in file
+ numbers should not be relied on.
+ """
+ LOG_DEBUG(f"tox.file_send")
+ tox_err_file_send = c_int()
+ if type(file_id) == bytes:
+ public_key = str(file_id, 'utf-8')
+ if type(filename) == bytes:
+ filename = str(filename, 'utf-8')
+ result = self.libtoxcore.tox_file_send(self._tox_pointer,
+ c_uint32(friend_number),
+ c_uint32(kind),
+ c_uint64(file_size),
+ string_to_bin_charp(file_id),
+ c_char_p(filename),
+ c_size_t(len(filename)),
+ byref(tox_err_file_send))
+ err_file = tox_err_file_send.value
+ if err_file == TOX_ERR_FILE_SEND['OK']:
+ # UINT32_MAX
+ return int(result)
+ if err_file == TOX_ERR_FILE_SEND['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if err_file == TOX_ERR_FILE_SEND['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if err_file == TOX_ERR_FILE_SEND['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if err_file == TOX_ERR_FILE_SEND['NAME_TOO_LONG']:
+ raise ArgumentError('Filename length exceeded TOX_MAX_FILENAME_LENGTH bytes.')
+ if err_file == TOX_ERR_FILE_SEND['TOO_MANY']:
+ raise ToxError('Too many ongoing transfers. The maximum number of concurrent file transfers is 256 per'
+ 'friend per direction (sending and receiving).')
+ raise ToxError('The function did not return OK')
+
+ def file_send_chunk(self, friend_number: int, file_number: int, position, data: str) -> int:
+ """
+ Send a chunk of file data to a friend.
+
+ This function is called in response to the `file_chunk_request` callback. The length parameter should be equal
+ to the one received though the callback. If it is zero, the transfer is assumed complete. For files with known
+ size, Core will know that the transfer is complete after the last byte has been received, so it is not necessary
+ (though not harmful) to send a zero-length chunk to terminate. For streams, core will know that the transfer is
+ finished if a chunk with length less than the length requested in the callback is sent.
+
+ :param friend_number: The friend number of the receiving friend for this file.
+ :param file_number: The file transfer identifier returned by tox_file_send.
+ :param position: The file or stream position from which to continue reading.
+ :param data: Chunk of file data
+ :return: true on success.
+ """
+ LOG_DEBUG(f"tox.file_send_chunk")
+ tox_err_file_send_chunk = c_int()
+
+ isinstance(data, Array), type(data)
+
+ result = self.libtoxcore.tox_file_send_chunk(self._tox_pointer,
+ c_uint32(friend_number), c_uint32(file_number),
+ c_uint64(position),
+ c_char_p(data),
+ c_size_t(len(data)),
+ byref(tox_err_file_send_chunk))
+ tox_err_file_send_chunk = tox_err_file_send_chunk.value
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['OK']:
+ return bool(result)
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NULL']:
+ raise ArgumentError('The length parameter was non-zero, but data was NULL.')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_FOUND']:
+ ArgumentError('The friend_number passed did not designate a valid friend.')
+ elif tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_FOUND']:
+ raise ArgumentError('No file transfer with the given file number was found for the given friend.')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['NOT_TRANSFERRING']:
+ raise ArgumentError('File transfer was found but isn\'t in a transferring state: (paused, done, broken, '
+ 'etc...) (happens only when not called from the request chunk callback).')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['INVALID_LENGTH']:
+ raise ArgumentError('Attempted to send more or less data than requested. The requested data size is '
+ '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.')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['SENDQ']:
+ raise ToxError('Packet queue is full.')
+ if tox_err_file_send_chunk == TOX_ERR_FILE_SEND_CHUNK['WRONG_POSITION']:
+ raise ArgumentError('Position parameter was wrong.')
+ raise ToxError('The function did not return OK')
+
+ def callback_file_chunk_request(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `file_chunk_request` event. Pass None to unset.
+
+ This event is triggered when Core is ready to send more file data.
+
+ :param callback: Python function.
+ If the length parameter is 0, the file transfer is finished, and
+ the client's resources associated with the file number should be
+ released. After a call with zero length, the file number can be
+ reused for future file transfers.
+
+ If the requested position is not equal to the client's idea of
+ the current file or stream position, it will need to seek. In
+ case of read-once streams, the client should keep the last read
+ chunk so that a seek back can be supported. A seek-back only
+ ever needs to read from the last requested chunk. This happens
+ when a chunk was requested, but the send failed. A seek-back
+ request can occur an arbitrary number of times for any given
+ chunk.
+
+ In response to receiving this callback, the client should call
+ the function `tox_file_send_chunk` with the requested chunk. If
+ the number of bytes sent through that function is zero, the file
+ transfer is assumed complete. A client must send the full length
+ of data requested with this callback.
+
+ Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the receiving friend for this file.
+ The file transfer identifier (c_uint32) returned by tox_file_send.
+ The file or stream position (c_uint64) from which to continue reading.
+ The number of bytes (c_size_t) requested for the current chunk.
+ pointer (c_void_p) to user_data
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_file_chunk_request(self._tox_pointer,
+ POINTER(None)())
+ self.file_chunk_request_cb = None
+ return
+ LOG_DEBUG(f"tox.callback_file_chunk_request")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, c_size_t, c_void_p)
+ self.file_chunk_request_cb = c_callback(callback)
+ self.libtoxcore.tox_callback_file_chunk_request(self._tox_pointer, self.file_chunk_request_cb)
+
+ # File transmission: receiving
+
+ def callback_file_recv(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `file_recv` event. Pass None to unset.
+
+ This event is triggered when a file transfer request is
+ received.
+
+ :param callback: Python function.
+ The client should acquire resources to be associated with the
+ file transfer. Incoming file transfers start in the PAUSED
+ state. After this callback returns, a transfer can be rejected
+ by sending a TOX_FILE_CONTROL_CANCEL control command before any
+ other control commands. It can be accepted by sending
+ TOX_FILE_CONTROL_RESUME.
+
+ Should take pointer (c_void_p) to Tox object,
+ The friend number (c_uint32) of the friend who is sending the file transfer request.
+ The friend-specific file number (c_uint32) the data received is associated with.
+ The meaning of the file (c_uint32) to be sent.
+ Size in bytes (c_uint64) of the file the client wants to send, UINT64_MAX if unknown or streaming.
+ Name of the file (c_char_p). Does not need to be the actual name. This name will be sent along with the file
+ send request.
+ Size in bytes (c_size_t) of the filename.
+ pointer (c_void_p) to user_data
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_file_recv(self._tox_pointer,
+ POINTER(None)())
+ self.file_recv_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_file_recv")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_uint64, c_char_p, c_size_t, c_void_p)
+ self.file_recv_cb = c_callback(callback)
+ self.libtoxcore.tox_callback_file_recv(self._tox_pointer, self.file_recv_cb)
+
+ def callback_file_recv_chunk(self, callback: Union[Callable,None]) -> None:
+ """Set the callback for the `file_recv_chunk` event. Pass NULL to unset.
+
+ This event is first triggered when a file transfer request is
+ received, and subsequently when a chunk of file data for an
+ accepted request was received.
+
+ :param callback: Python function.
+ When length is 0, the transfer is finished and the client should
+ release the resources it acquired for the transfer. After a call
+ with length = 0, the file number can be reused for new file
+ transfers.
+
+ If position is equal to file_size (received in the file_receive
+ callback) when the transfer finishes, the file was received
+ completely. Otherwise, if file_size was UINT64_MAX, streaming
+ ended successfully when length is 0.
+
+ Should take pointer (c_void_p) to Tox object, The friend number
+ (c_uint32) of the friend who is sending the file. The
+ friend-specific file number (c_uint32) the data received is
+ associated with. The file position (c_uint64) of the first byte
+ in data. A byte array (c_char_p) containing the received chunk.
+ The length (c_size_t) of the received chunk. pointer (c_void_p)
+ to user_data
+ """
+ if callback is None:
+ Tox.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer,
+ POINTER(None)())
+ self.file_recv_chunk_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_file_recv_chunk")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, POINTER(c_uint8), c_size_t, c_void_p)
+ self.file_recv_chunk_cb = c_callback(callback)
+ self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb)
+
+ # Low-level custom packet sending and receiving
+
+ def friend_send_lossy_packet(self, friend_number: int, data: bytes) -> bool:
+ """
+ Send a custom lossy packet to a friend.
+ The first byte of data must be in the range 200-254. Maximum length of a
+ custom packet is TOX_MAX_CUSTOM_PACKET_SIZE.
+
+ Lossy packets behave like UDP packets, meaning they might never reach the
+ other side or might arrive more than once (if someone is messing with the
+ connection) or might arrive in the wrong order.
+
+ Unless latency is an issue, it is recommended that you use lossless custom packets instead.
+
+ :param friend_number: The friend number of the friend this lossy packet
+ :param data: python string containing the packet data
+ :return: True on success.
+ """
+ LOG_DEBUG(f"friend_send_lossy_packet")
+ isinstance(data, Array), type(data)
+ tox_err_friend_custom_packet = c_int()
+ result = self.libtoxcore.tox_friend_send_lossy_packet(self._tox_pointer,
+ c_uint32(friend_number),
+ c_char_p(data),
+ c_size_t(len(data)),
+ byref(tox_err_friend_custom_packet))
+ tox_err_friend_custom_packet = tox_err_friend_custom_packet.value
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']:
+ return bool(result)
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']:
+ raise ArgumentError('The first byte of data was not in the specified range for the packet type.'
+ 'This range is 200-254 for lossy, and 160-191 for lossless packets.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']:
+ raise ArgumentError('Attempted to send an empty packet.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
+ raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']:
+ raise ToxError('Packet queue is full.')
+ raise ToxError('The function did not return OK')
+
+ def friend_send_lossless_packet(self, friend_number: int, data: bytes) -> int:
+ """
+ Send a custom lossless packet to a friend.
+ The first byte of data must be in the range 160-191. Maximum length of a
+ custom packet is TOX_MAX_CUSTOM_PACKET_SIZE.
+
+ Lossless packet behaviour is comparable to TCP (reliability, arrive in order)
+ but with packets instead of a stream.
+
+ :param friend_number: The friend number of the friend this lossless packet
+ :param data: python string containing the packet data
+ :return: True on success.
+ """
+ LOG_DEBUG(f"friend_send_lossless_packet")
+ tox_err_friend_custom_packet = c_int()
+ isinstance(data, Array), type(data)
+ result = self.libtoxcore.tox_friend_send_lossless_packet(self._tox_pointer,
+ c_uint32(friend_number),
+ c_char_p(data),
+ c_size_t(len(data)),
+ byref(tox_err_friend_custom_packet))
+ tox_err_friend_custom_packet = tox_err_friend_custom_packet.value
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['OK']:
+ return bool(result)
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('This client is currently not connected to the friend.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['INVALID']:
+ raise ArgumentError('The first byte of data was not in the specified range for the packet type.'
+ 'This range is 200-254 for lossy, and 160-191 for lossless packets.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['EMPTY']:
+ raise ArgumentError('Attempted to send an empty packet.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['TOO_LONG']:
+ raise ArgumentError('Packet data length exceeded TOX_MAX_CUSTOM_PACKET_SIZE.')
+ if tox_err_friend_custom_packet == TOX_ERR_FRIEND_CUSTOM_PACKET['SENDQ']:
+ raise ToxError('Packet queue is full.')
+ raise ToxError('The function did not return OK')
+
+ def callback_friend_lossy_packet(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_lossy_packet` event. Pass NULL to unset.
+
+ :param callback: Python function.
+ Should take pointer (c_void_p) to Tox object,
+ friend_number (c_uint32) - The friend number of the friend who sent a lossy packet,
+ A byte array (c_uint8 array) containing the received packet data,
+ length (c_size_t) - The length of the packet data byte array,
+ pointer (c_void_p) to user_data
+ """
+ if callback is None:
+ self.libtoxcore.tox_callback_friend_lossy_packet(self._tox_pointer, POINTER(None)())
+ self.friend_lossy_packet_cb = None
+ return
+
+ LOG_DEBUG(f"callback_friend_lossy_packet")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p)
+ self.friend_lossy_packet_cb = c_callback(callback)
+ self.libtoxcore.tox_callback_friend_lossy_packet(self._tox_pointer, self.friend_lossy_packet_cb)
+
+ def callback_friend_lossless_packet(self, callback: Union[Callable,None]) -> None:
+ """
+ Set the callback for the `friend_lossless_packet` event. Pass NULL to unset.
+
+ :param callback: Python function.
+ Should take pointer (c_void_p) to Tox object,
+ friend_number (c_uint32) - The friend number of the friend who sent a lossless packet,
+ A byte array (c_uint8 array) containing the received packet data,
+ length (c_size_t) - The length of the packet data byte array,
+ pointer (c_void_p) to user_data
+ """
+ if callback is None:
+ self.friend_lossless_packet_cb = None
+ self.libtoxcore.tox_callback_friend_lossless_packet(self._tox_pointer, POINTER(None)())
+ return
+
+ LOG_DEBUG(f"callback_friend_lossless_packet")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_void_p)
+ self.friend_lossless_packet_cb = c_callback(callback)
+ self.libtoxcore.tox_callback_friend_lossless_packet(self._tox_pointer, self.friend_lossless_packet_cb)
+
+ # Low-level network information
+ # def self_get_keys(self): pass
+
+ def self_get_dht_id(self, dht_id=None) -> str:
+ """Writes the temporary DHT public key of this instance to a byte array.
+
+ This can be used in combination with an externally accessible
+ IP address and the bound port (from tox_self_get_udp_port) to
+ run a temporary bootstrap node.
+
+ Be aware that every time a new instance is created, the DHT
+ public key changes, meaning this cannot be used to run a
+ permanent bootstrap node.
+
+ :param dht_id: pointer (c_char_p) to a memory region of at least TOX_PUBLIC_KEY_SIZE bytes. If this parameter is
+ None, this function allocates memory for dht_id.
+ :return: dht_id
+
+ """
+ if dht_id is None:
+ dht_id = create_string_buffer(TOX_PUBLIC_KEY_SIZE)
+ else:
+ isinstance(dht_id, Array), type(dht_id)
+ LOG_DEBUG(f"tox.self_get_dht_id")
+ Tox.libtoxcore.tox_self_get_dht_id(self._tox_pointer, dht_id)
+ return bin_to_string(dht_id, TOX_PUBLIC_KEY_SIZE)
+
+ def self_get_udp_port(self) -> int:
+ """
+ Return the UDP port this Tox instance is bound to.
+ """
+ tox_err_get_port = c_int()
+ LOG_DEBUG(f"tox.self_get_udp_port")
+ result = Tox.libtoxcore.tox_self_get_udp_port(self._tox_pointer, byref(tox_err_get_port))
+ tox_err_get_port = tox_err_get_port.value
+ if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
+ return int(result)
+ if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
+ raise ToxError('The instance was not bound to any port.')
+ raise ToxError('The function did not return OK')
+
+ def self_get_tcp_port(self) -> int:
+ """
+ Return the TCP port this Tox instance is bound to. This is only relevant if the instance is acting as a TCP
+ relay.
+ """
+ tox_err_get_port = c_int()
+ LOG_DEBUG(f"tox.self_get_tcp_port")
+ result = Tox.libtoxcore.tox_self_get_tcp_port(self._tox_pointer, byref(tox_err_get_port))
+ tox_err_get_port = tox_err_get_port.value
+ if tox_err_get_port == TOX_ERR_GET_PORT['OK']:
+ return int(result)
+ if tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']:
+ raise ToxError('The instance was not bound to any port.')
+ raise ToxError('The function did not return OK')
+
+ # Group chat instance management
+
+ def group_new(self, privacy_state: int, group_name: Union[bytes,str], nick: Union[bytes,str], status: str='') -> int:
+ """Creates a new group chat.
+
+ This function creates a new group chat object and adds it to the chats array.
+
+ The client should initiate its peer list with self info after
+ calling this function, as the peer_join callback will not be
+ triggered.
+
+ :param privacy_state: The privacy state of the group. If this is set to TOX_GROUP_PRIVACY_STATE_PUBLIC,
+ the group will attempt to announce itself to the DHT and anyone with the Chat ID may join.
+ Otherwise a friend invite will be required to join the group.
+ :param group_name: The name of the group. The name must be non-NULL.
+
+ :return group number on success, UINT32_MAX on failure.
+
+ """
+
+ LOG_DEBUG(f"tox.group_new")
+ error = c_int()
+ if type(nick) == str:
+ nick = bytes(nick, 'utf-8')
+ if type(group_name) == str:
+ group_name = bytes(group_name, 'utf-8')
+
+ result = Tox.libtoxcore.tox_group_new(self._tox_pointer,
+ privacy_state,
+ c_char_p(group_name),
+ c_size_t(len(group_name)),
+ nick,
+ c_size_t(len(nick)),
+ byref(error))
+
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_NEW)
+ LOG_ERROR(f"group_new err={error.value} {s}")
+ raise ToxError(f"group_new {s} err={error.value}")
+
+ # TypeError: '<' not supported between instances of 'c_uint' and 'int'
+ return int(result)
+
+ def group_join(self, chat_id, password: Union[bytes,str], nick: Union[bytes,str], status='') -> int:
+ """Joins a group chat with specified Chat ID.
+
+ This function creates a new group chat object, adds it to the
+ chats array, and sends a DHT announcement to find peers in the
+ group associated with chat_id. Once a peer has been found a
+ join attempt will be initiated.
+
+ :param chat_id: The Chat ID of the group you wish to join. This must be TOX_GROUP_CHAT_ID_SIZE bytes.
+ :param password: The password required to join the group. Set to NULL if no password is required.
+ :param status: FixMe
+
+ :return group_number on success, UINT32_MAX on failure.
+ """
+
+ LOG_DEBUG(f"tox.group_join")
+ assert chat_id, chat_id
+ assert nick, nick
+ error = c_int()
+ if type(nick) == str:
+ nick = bytes(nick, 'utf-8')
+ if True: # API change
+ if not password:
+ cpassword = None
+ else:
+ if password and type(password) == str:
+ nick = bytes(password, 'utf-8')
+#?no cpassword = c_char_p(password) # it's const uint8_t *password
+ cpassword = password
+ result = Tox.libtoxcore.tox_group_join(self._tox_pointer,
+ string_to_bin_charp(chat_id),
+ c_char_p(nick),
+ c_size_t(len(nick)),
+ cpassword,
+ c_size_t(len(password)) if password else 0,
+
+ byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_JOIN)
+ LOG_ERROR(f"group_new err={error.value} {s}")
+ raise ToxError(f"group_new {s} err={error.value}")
+ LOG_INFO(f"group_new result={result} chat_id={chat_id}")
+
+ return int(result)
+
+ def group_reconnect(self, group_number) -> bool:
+ """
+ Reconnects to a group.
+
+ This function disconnects from all peers in the group, then attempts to reconnect with the group.
+ The caller's state is not changed (i.e. name, status, role, chat public key etc.)
+
+ :param group_number: The group number of the group we wish to reconnect to.
+ :return True on success.
+ """
+
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ error = c_int()
+ LOG_DEBUG(f"tox.group_reconnect")
+ result = Tox.libtoxcore.tox_group_reconnect(self._tox_pointer,
+ c_uint32(group_number),
+ byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_RECONNECT)
+ LOG_ERROR(f"group_new err={error.value} {s}")
+ raise ToxError(f"group_new {s} err={error.value}")
+ return bool(result)
+
+ def group_is_connected(self, group_number) -> bool:
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_is_connected")
+ result = Tox.libtoxcore.tox_group_is_connected(self._tox_pointer, c_uint32(group_number), byref(error))
+ if error.value:
+ # TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND
+ s = sGetError(error.value, TOX_ERR_GROUP_IS_CONNECTED)
+ LOG_ERROR(f"group_new err={error.value} {s}")
+ raise ToxError("group_is_connected err={error.value} {s}")
+ return bool(result)
+
+ def group_disconnect(self, group_number: int) -> bool:
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ error = c_int()
+ LOG_DEBUG(f"tox.group_disconnect")
+ result = Tox.libtoxcore.tox_group_disconnect(self._tox_pointer, c_uint32(group_number), byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_DISCONNECT)
+ LOG_ERROR(f"group_disconnect err={error.value} {s}")
+ raise ToxError(f"group_disconnect {s} err={error.value}")
+ return bool(result)
+
+ def group_leave(self, group_number: int, message: Union[str,None]=None) -> bool:
+ """Leaves a group.
+
+ This function sends a parting packet containing a custom
+ (non-obligatory) message to all peers in a group, and deletes
+ the group from the chat array. All group state information is
+ permanently lost, including keys and role credentials.
+
+ :param group_number: The group number of the group we wish to leave.
+ :param message: The parting message to be sent to all the peers. Set to NULL if we do not wish to
+ send a parting message.
+
+ :return True if the group chat instance was successfully deleted.
+
+ """
+
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ LOG_DEBUG(f"tox.leave")
+ error = c_int()
+ f = Tox.libtoxcore.tox_group_leave
+ f.restype = c_bool
+ if message is not None and type(message) == str:
+ message = bytes(message, 'utf-8')
+ result = f(self._tox_pointer, c_uint32(group_number), message,
+ c_size_t(len(message)) if message else 0, byref(error))
+ if error.value:
+ LOG_ERROR(f"group_leave err={error.value}")
+ raise ToxError("group_leave err={error.value}")
+ return bool(result)
+
+ # Group user-visible client information (nickname/status/role/public key)
+
+ def group_self_set_name(self, group_number: int, name) -> bool:
+ """Set the client's nickname for the group instance designated by the given group number.
+
+ Nickname length cannot exceed TOX_MAX_NAME_LENGTH. If length
+ is equal to zero or name is a NULL pointer, the function call
+ will fail.
+
+ :param name: A byte array containing the new nickname.
+
+ :return True on success.
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ if type(name) == str:
+ name = bytes(name, 'utf-8') # not c_char_p()
+ LOG_DEBUG(f"tox.group_self_set_name")
+ result = Tox.libtoxcore.tox_group_self_set_name(self._tox_pointer,
+ c_uint32(group_number),
+ name, c_size_t(len(name)),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_set_name err={error.value}")
+ raise ToxError("group_self_set_name err={error.value}")
+ return bool(result)
+
+ def group_self_get_name_size(self, group_number: int) -> int:
+ """
+ Return the length of the client's current nickname for the group instance designated
+ by group_number as passed to tox_group_self_set_name.
+
+ If no nickname was set before calling this function, the name is empty,
+ and this function returns 0.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_TRACE(f"tox_group_self_get_name_size")
+ result = Tox.libtoxcore.tox_group_self_get_name_size(self._tox_pointer,
+ c_uint32(group_number),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_get_name_size err={error.value}")
+ raise ToxError("group_self_get_name_size err={error.value}")
+ return int(result)
+
+ def group_self_get_name(self, group_number: int) -> str:
+ """Write the nickname set by tox_group_self_set_name to a byte array.
+
+ If no nickname was set before calling this function, the name is empty,
+ and this function has no effect.
+
+ Call tox_group_self_get_name_size to find out how much memory
+ to allocate for the result.
+
+ :return nickname
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ size = self.group_self_get_name_size(group_number)
+ name = create_string_buffer(size)
+ LOG_DEBUG(f"tox.group_self_get_name")
+ result = Tox.libtoxcore.tox_group_self_get_name(self._tox_pointer,
+ c_uint32(group_number),
+ name,
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_get_name err={error.value}")
+ raise ToxError("group_self_get_name err={error.value}")
+ return str(name[:size], 'utf-8', errors='ignore')
+
+ def group_self_set_status(self, group_number: int, status: int) -> bool:
+
+ """
+ Set the client's status for the group instance. Status must be a TOX_USER_STATUS.
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_self_set_status")
+ result = Tox.libtoxcore.tox_group_self_set_status(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(status),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_set_status err={error.value}")
+ raise ToxError("group_self_set_status err={error.value}")
+ return bool(result)
+
+ def group_self_get_status(self, group_number: int) -> int:
+ """
+ returns the client's status for the group instance on success.
+ return value is unspecified on failure.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_self_get_status")
+ result = Tox.libtoxcore.tox_group_self_get_status(self._tox_pointer, c_uint32(group_number), byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_get_status err={error.value}")
+ raise ToxError("group_self_get_status err={error.value}")
+ return int(result)
+
+ def group_self_get_role(self, group_number: int) -> int:
+ """
+ returns the client's role for the group instance on success.
+ return value is unspecified on failure.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_self_get_role")
+ result = Tox.libtoxcore.tox_group_self_get_role(self._tox_pointer, c_uint32(group_number), byref(error))
+ if error.value:
+ LOG_ERROR(f"group_self_get_role err={error.value}")
+ raise ToxError(f"group_self_get_role err={error.value}")
+ return int(result)
+
+ def group_self_get_peer_id(self, group_number: int) -> int:
+ """
+ returns the client's peer id for the group instance on success.
+ return value is unspecified on failure.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_self_get_peer_id")
+ result = Tox.libtoxcore.tox_group_self_get_peer_id(self._tox_pointer, c_uint32(group_number), byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_self_get_peer_id err={error.value}")
+ raise ToxError("tox_group_self_get_peer_id err={error.value}")
+ return int(result)
+
+ def group_self_get_public_key(self, group_number: int) -> str:
+ """Write the client's group public key designated by the given group number to a byte array.
+s
+ This key will be permanently tied to the client's identity for
+ this particular group until the client explicitly leaves the
+ group or gets kicked/banned. This key is the only way for other
+ peers to reliably identify the client across client restarts.
+
+ `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes.
+
+ :return public key
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
+ LOG_DEBUG(f"tox.group_self_get_public_key")
+ result = Tox.libtoxcore.tox_group_self_get_public_key(self._tox_pointer,
+ c_uint32(group_number),
+ key, byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_self_get_public_key {TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
+ raise ToxError(f"tox.group_self_get_public_key {TOX_ERR_FRIEND_GET_PUBLIC_KEY[error.value]}")
+ return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
+
+ # Peer-specific group state queries.
+
+ def group_peer_get_name_size(self, group_number: int, peer_id: int) -> int:
+ """
+ Return the length of the peer's name. If the group number or ID is invalid, the
+ return value is unspecified.
+
+ The return value is equal to the `length` argument received by the last
+ `group_peer_name` callback.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ result = Tox.libtoxcore.tox_group_peer_get_name_size(self._tox_pointer, c_uint32(group_number), c_uint32(peer_id), byref(error))
+ if error.value:
+ LOG_ERROR(f" err={error.value}")
+ raise ToxError(f" err={error.value}")
+ LOG_TRACE(f"tox_group_peer_get_name_size")
+ return int(result)
+
+ def group_peer_get_name(self, group_number: int, peer_id: int) -> str:
+ """Write the name of the peer designated by the given ID to a byte
+ array.
+
+ Call tox_group_peer_get_name_size to determine the allocation
+ size for the `name` parameter.
+
+ The data written to `name` is equal to the data received by the last
+ `group_peer_name` callback.
+
+ :param group_number: The group number of the group we wish to query.
+ :param peer_id: The ID of the peer whose name we want to retrieve.
+
+ :return name.
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ error = c_int()
+ size = self.group_peer_get_name_size(group_number, peer_id)
+ name = create_string_buffer(size)
+ LOG_DEBUG(f"tox.group_peer_get_name")
+ result = Tox.libtoxcore.tox_group_peer_get_name(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(peer_id),
+ name, byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_peer_get_name err={error.value}")
+ raise ToxError(f"tox_group_peer_get_name err={error.value}")
+ sRet = str(name[:], 'utf-8', errors='ignore')
+ return sRet
+
+ def group_peer_get_status(self, group_number: int, peer_id: int) -> int:
+ """
+ Return the peer's user status (away/busy/...). If the ID or group number is
+ invalid, the return value is unspecified.
+
+ The status returned is equal to the last status received through the
+ `group_peer_status` callback.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 group_number={group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_peer_get_status")
+ result = Tox.libtoxcore.tox_group_peer_get_status(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(peer_id),
+ byref(error))
+ if error.value:
+ # unwrapped
+ LOG_ERROR(f"tox.group_peer_get_status err={error.value}")
+ raise ToxError(f"tox.group_peer_get_status err={error.value}")
+ return int(result)
+
+ def group_peer_get_role(self, group_number: int, peer_id: int) -> int:
+ """
+ Return the peer's role (user/moderator/founder...). If the ID or group number is
+ invalid, the return value is unspecified.
+
+ The role returned is equal to the last role received through the
+ `group_moderation` callback.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_peer_get_role")
+ result = Tox.libtoxcore.tox_group_peer_get_role(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(peer_id),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_peer_get_role err={error.value}")
+ raise ToxError(f"tox.group_peer_get_role err={error.value}")
+ return int(result)
+
+ def group_peer_get_public_key(self, group_number: int, peer_id: int) -> str:
+ """Write the group public key with the designated peer_id for the designated group number to public_key.
+
+ This key will be permanently tied to a particular peer until
+ they explicitly leave the group or get kicked/banned, and is
+ the only way to reliably identify the same peer across client
+ restarts.
+
+ `public_key` should have room for at least TOX_GROUP_PEER_PUBLIC_KEY_SIZE bytes.
+
+ :return public key
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ key = create_string_buffer(TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
+ LOG_DEBUG(f"tox.group_peer_get_public_key")
+ result = Tox.libtoxcore.tox_group_peer_get_public_key(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(peer_id),
+ key, byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_peer_get_public_key err={error.value}")
+ raise ToxError(f"tox.group_peer_get_public_key err={error.value}")
+ return bin_to_string(key, TOX_GROUP_PEER_PUBLIC_KEY_SIZE)
+
+ def callback_group_peer_name(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_peer_name` event. Pass NULL to unset.
+ This event is triggered when a peer changes their nickname.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_peer_name(self._tox_pointer,
+ POINTER(None)(), user_data)
+ self.group_peer_name_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_peer_name")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p)
+ self.group_peer_name_cb = c_callback(callback)
+ try:
+ Tox.libtoxcore.tox_callback_group_peer_name(self._tox_pointer, self.group_peer_name_cb)
+ except Exception as e: # AttributeError
+ LOG_ERROR(f"tox.callback_conference_peer_name {e}")
+
+ def callback_group_peer_status(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_peer_status` event. Pass NULL to unset.
+ This event is triggered when a peer changes their status.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_peer_status(self._tox_pointer, POINTER(None)())
+ self.group_peer_status_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_peer_status")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_void_p)
+ #* @param group_number The group number of the group we wish to query.
+ #* @param peer_id The ID of the peer whose status we wish to query.
+ # *error
+ self.group_peer_status_cb = c_callback(callback)
+ try:
+ Tox.libtoxcore.tox_callback_group_peer_status(self._tox_pointer, self.group_peer_status_cb)
+ except Exception as e:
+ LOG_ERROR(f"callback_group_peer_status EXCEPTION {e}")
+
+
+ # Group chat state queries and events.
+
+ def group_set_topic(self, group_number: int, topic: str) -> bool:
+ """Set the group topic and broadcast it to the rest of the group.
+
+ topic length cannot be longer than TOX_GROUP_MAX_TOPIC_LENGTH.
+ If length is equal to zero or topic is set to NULL, the topic will be unset.
+
+ :return True on success.
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ if type(topic) == str:
+ topic = bytes(topic, 'utf-8') # not c_char_p()
+ try:
+ LOG_DEBUG(f"tox.group_set_topic")
+ result = Tox.libtoxcore.tox_group_set_topic(self._tox_pointer,
+ c_uint32(group_number),
+ topic,
+ c_size_t(len(topic)),
+ byref(error))
+ except Exception as e:
+ LOG_WARN(f"group_set_topic EXCEPTION {e}")
+ raise
+ if error.value:
+ LOG_ERROR(f"group_set_topic err={error.value}")
+ raise ToxError("group_set_topic err={error.value}")
+ return bool(result)
+
+ def group_get_topic_size(self, group_number: int) -> int:
+ """
+ Return the length of the group topic. If the group number is invalid, the
+ return value is unspecified.
+
+ The return value is equal to the `length` argument received by the last
+ `group_topic` callback.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_TRACE(f"tox_group_get_topic_size")
+ try:
+ result = Tox.libtoxcore.tox_group_get_topic_size(self._tox_pointer,
+ c_uint32(group_number),
+ byref(error))
+ except Exception as e:
+ LOG_ERROR(f"group_get_topic_size EXCEPTION {e}")
+ raise
+ if error.value:
+ LOG_ERROR(f"tox_group_get_topic_size err={error.value}")
+ raise ToxError(f"tox_group_get_topic_size err={error.value}")
+ return int(result)
+
+ def group_get_topic(self, group_number: int) -> str:
+ """
+ Write the topic designated by the given group number to a byte array.
+ Call tox_group_get_topic_size to determine the allocation size for the `topic` parameter.
+ The data written to `topic` is equal to the data received by the last
+ `group_topic` callback.
+
+ :return topic
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ size = self.group_get_topic_size(group_number)
+ topic = create_string_buffer(size)
+ LOG_DEBUG(f"tox.group_get_topic")
+ Tox.libtoxcore.tox_group_get_topic(self._tox_pointer,
+ c_uint32(group_number),
+ topic, byref(error))
+ if error.value:
+ LOG_ERROR(f"group_get_topic err={error.value}")
+ raise ToxError(f"group_get_topic err={error.value}")
+ return str(topic[:size], 'utf-8', errors='ignore')
+
+ def group_get_name_size(self, group_number: int) -> int:
+ """
+ Return the length of the group name. If the group number is invalid, the
+ return value is unspecified.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ error = c_int()
+ result = Tox.libtoxcore.tox_group_get_name_size(self._tox_pointer,
+ c_uint32(group_number),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_get_name_size err={error.value}")
+ raise ToxError(f"group_get_name_size err={error.value}")
+ return int(result)
+
+ def group_get_name(self, group_number: int) -> str:
+ """
+ Write the name of the group designated by the given group number to a byte array.
+ Call tox_group_get_name_size to determine the allocation size for the `name` parameter.
+ :return true on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ size = self.group_get_name_size(group_number)
+ name = create_string_buffer(size)
+ LOG_DEBUG(f"tox.group_get_name")
+ result = Tox.libtoxcore.tox_group_get_name(self._tox_pointer,
+ c_uint32(group_number),
+ name, byref(error))
+ if error.value:
+ LOG_ERROR(f"group_get_name err={error.value}")
+ raise ToxError(f"group_get_name err={error.value}")
+ return str(name[:size], 'utf-8', errors='ignore')
+
+ def group_get_chat_id(self, group_number: int) -> str:
+ """
+ Write the Chat ID designated by the given group number to a byte array.
+ `chat_id` should have room for at least TOX_GROUP_CHAT_ID_SIZE bytes.
+ :return chat id. or None if not found.
+ """
+ LOG_INFO(f"tox.group_get_chat_id group_number={group_number}")
+ if group_number < 0:
+ LOG_ERROR(f"group_get_chat_id group_number < 0 group_number={group_number}")
+ raise ToxError(f"group_get_chat_id group_number < 0 group_number={group_number}")
+
+ error = c_int()
+ buff = create_string_buffer(TOX_GROUP_CHAT_ID_SIZE)
+ result = Tox.libtoxcore.tox_group_get_chat_id(self._tox_pointer,
+ c_uint32(group_number),
+ buff, byref(error))
+ if error.value:
+ if error.value == 1:
+ LOG_ERROR(f"tox.group_get_chat_id ERROR GROUP_STATE_QUERIES_GROUP_NOT_FxOUND group_number={group_number}")
+ else:
+ LOG_ERROR(f"tox.group_get_chat_id group_number={group_number} err={error.value}")
+ raise ToxError(f"tox_group_get_chat_id err={error.value} group_number={group_number}")
+#
+# 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
+ result = bin_to_string(buff, TOX_GROUP_CHAT_ID_SIZE)
+ LOG_DEBUG(f"tox.group_get_chat_id group_number={group_number} result={result}")
+
+ return result
+
+ def group_get_number_groups(self) -> int:
+ """
+ Return the number of groups in the Tox chats array.
+ """
+ LOG_DEBUG(f"tox.group_get_number_groups")
+ try:
+ result = Tox.libtoxcore.tox_group_get_number_groups(self._tox_pointer)
+ except Exception as e:
+ LOG_WARN(f"tox.group_get_number_groups EXCEPTION {e}")
+ result = 0
+ LOG_INFO(f"tox.group_get_number_groups returning {result}")
+ return int(result)
+
+ def groups_get_list(self):
+ raise NotImplementedError('tox_groups_get_list')
+# groups_list_size = self.group_get_number_groups()
+# groups_list = create_string_buffer(sizeof(c_uint32) * groups_list_size)
+# groups_list = POINTER(c_uint32)(groups_list)
+# LOG_DEBUG(f"tox.groups_get_list")
+# Tox.libtoxcore.tox_groups_get_list(self._tox_pointer, groups_list)
+# return groups_list[0:groups_list_size]
+
+ def group_get_privacy_state(self, group_number: int) -> int:
+ """
+ Return the privacy state of the group designated by the given group number. If group number
+ is invalid, the return value is unspecified.
+
+ The value returned is equal to the data received by the last
+ `group_privacy_state` callback.
+
+ see the `Group chat founder controls` section for the respective set function.
+ """
+ if group_number < 0:
+ raise ToxError(f"group_get_privacy_state group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_get_privacy_state")
+ result = Tox.libtoxcore.tox_group_get_privacy_state(self._tox_pointer,
+ c_uint32(group_number),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_get_privacy_state err={error.value}")
+ raise ToxError(f"tox.group_get_privacy_state err={error.value}")
+ return int(result)
+
+ def group_get_peer_limit(self, group_number: int) -> int:
+ """
+ Return the maximum number of peers allowed for the group designated by the given group number.
+ If the group number is invalid, the return value is unspecified.
+
+ The value returned is equal to the data received by the last
+ `group_peer_limit` callback.
+
+ see the `Group chat founder controls` section for the respective set function.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_get_peer_limit")
+ result = Tox.libtoxcore.tox_group_get_peer_limit(self._tox_pointer,
+ c_uint(group_number),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_get_peer_limit err={error.value}")
+ raise ToxError(f"tox.group_get_peer_limit err={error.value}")
+ return int(result)
+
+ def group_get_password_size(self, group_number: int) -> int:
+ """
+ Return the length of the group password. If the group number is invalid, the
+ return value is unspecified.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_TRACE(f"tox_group_get_password_size")
+ result = Tox.libtoxcore.tox_group_get_password_size(self._tox_pointer,
+ c_uint(group_number), byref(error))
+ if error.value:
+ LOG_ERROR(f"group_get_password_size err={error.value}")
+ raise ToxError(f"group_get_password_size err={error.value}")
+ return result
+
+ def group_get_password(self, group_number: int) -> str:
+ """
+ Write the password for the group designated by the given group number to a byte array.
+
+ Call tox_group_get_password_size to determine the allocation size for the `password` parameter.
+
+ The data received is equal to the data received by the last
+ `group_password` callback.
+
+ see the `Group chat founder controls` section for the respective set function.
+
+ :return password
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ size = self.group_get_password_size(group_number)
+ password = create_string_buffer(size)
+ LOG_DEBUG(f"tox.group_get_password")
+ result = Tox.libtoxcore.tox_group_get_password(self._tox_pointer,
+ c_uint(group_number),
+ password, byref(error))
+ if error.value:
+ LOG_ERROR(f"group_get_password err={error.value}")
+ raise ToxError(f"group_get_password err={error.value}")
+ return str(password[:size], 'utf-8', errors='ignore')
+
+ def callback_group_topic(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_topic` event. Pass NULL to unset.
+ This event is triggered when a peer changes the group topic.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ LOG_DEBUG(f"tox.callback_group_topic")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_topic(self._tox_pointer, POINTER(None)())
+ self.group_topic_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_char_p, c_size_t, c_void_p)
+ self.group_topic_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_topic")
+ Tox.libtoxcore.tox_callback_group_topic(self._tox_pointer, self.group_topic_cb)
+ except Exception as e:
+ LOG_WARN(f" Exception {e}")
+
+ def callback_group_privacy_state(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_privacy_state` event. Pass NULL to unset.
+ This event is triggered when the group founder changes the privacy state.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ LOG_DEBUG(f"tox.callback_group_privacy_state")
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_privacy_state(self._tox_pointer, POINTER(None)())
+ self.group_privacy_state_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_void_p)
+ self.group_privacy_state_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_privacy_state")
+ Tox.libtoxcore.tox_callback_group_privacy_state(self._tox_pointer, self.group_privacy_state_cb)
+ except Exception as e:
+ LOG_WARN(f" Exception {e}")
+
+ def callback_group_peer_limit(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_peer_limit` event. Pass NULL to unset.
+ This event is triggered when the group founder changes the maximum peer limit.
+ """
+
+ LOG_DEBUG(f"tox.callback_group_peer_limit")
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_peer_limit(self._tox_pointer, POINTER(None)())
+ self.group_peer_limit_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p)
+ self.group_peer_limit_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_peer_limit")
+ Tox.libtoxcore.tox_callback_group_peer_limit(self._tox_pointer, self.group_peer_limit_cb)
+ except Exception as e:
+ LOG_WARN(f" Exception {e}")
+
+ def callback_group_password(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_password` event. Pass NULL to unset.
+ This event is triggered when the group founder changes the group password.
+ """
+
+ LOG_DEBUG(f"tox.callback_group_password")
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_password(self._tox_pointer, POINTER(None)())
+ self.group_password_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_char_p, c_size_t, c_void_p)
+ self.group_password_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_password")
+ Tox.libtoxcore.tox_callback_group_password(self._tox_pointer, self.group_password_cb)
+ except Exception as e:
+ LOG_WARN(f"tox.callback_group_password Exception {e}")
+
+ # Group message sending
+
+ def group_send_custom_packet(self, group_number: int, lossless: bool, data: bytes) -> bool:
+ """Send a custom packet to the group.
+
+ If lossless is true the packet will be lossless. Lossless
+ packet behaviour is comparable to TCP (reliability, arrive in
+ order) but with packets instead of a stream.
+
+ If lossless is false, the packet will be lossy. Lossy packets
+ behave like UDP packets, meaning they might never reach the
+ other side or might arrive more than once (if someone is
+ messing with the connection) or might arrive in the wrong
+ order.
+
+ Unless latency is an issue or message reliability is not
+ important, it is recommended that you use lossless custom
+ packets.
+
+ :param group_number: The group number of the group the message is intended for.
+ :param lossless: True if the packet should be lossless.
+ :param data A byte array containing the packet data.
+ :return True on success.
+
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+ isinstance(data, Array), type(data)
+ error = c_int()
+ LOG_DEBUG(f"tox.group_send_custom_packet")
+ result = Tox.libtoxcore.tox_group_send_custom_packet(self._tox_pointer,
+ c_uint(group_number),
+ lossless,
+ c_char_p(data),
+ c_size_t(len(data)),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_send_custom_packet err={error.value}")
+ raise ToxError(f"group_send_custom_packet err={error.value}")
+ return bool(result)
+
+ def group_send_private_message(self, group_number: int, peer_id: int, message_type: int, message: str) -> bool:
+ """
+ Send a text chat message to the specified peer in the specified group.
+
+ This function creates a group private message packet and pushes it into the send
+ queue.
+
+ The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages
+ must be split by the client and sent as separate messages. Other clients can
+ then reassemble the fragments. Messages may not be empty.
+
+ :param group_number: The group number of the group the message is intended for.
+ :param peer_id: The ID of the peer the message is intended for.
+ :param message: A non-NULL pointer to the first element of a byte array containing the message text.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ if type(message) == str:
+ message = bytes(message, 'utf-8') # not c_char_p
+ error = c_int()
+ LOG_DEBUG(f"group_send_private_message")
+ result = Tox.libtoxcore.tox_group_send_private_message(self._tox_pointer,
+ c_uint(group_number),
+ c_uint32(peer_id),
+ c_uint32(message_type),
+ message,
+ c_size_t(len(message)),
+ byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE)
+ LOG_ERROR(f"group_send_private_message err={error.value} {s}")
+ raise ToxError(f"group_send_private_message err={error.value} {s}")
+
+ return bool(result)
+
+ def group_send_message(self, group_number: int, message_type: int, message: str) -> bool:
+ """
+ Send a text chat message to the group.
+
+ This function creates a group message packet and pushes it into the send
+ queue.
+
+ The message length may not exceed TOX_MAX_MESSAGE_LENGTH. Larger messages
+ must be split by the client and sent as separate messages. Other clients can
+ then reassemble the fragments. Messages may not be empty.
+
+ :param group_number: The group number of the group the message is intended for.
+ :param message_type: Message type (normal, action, ...).
+ :param message: A non-NULL pointer to the first element of a byte array containing the message text.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ # uint32_t message_id = 0;
+ message_id = c_int() # or POINTER(None)()
+ if type(message) != bytes:
+ message = bytes(message, 'utf-8') # not c_char_p()
+ LOG_DEBUG(f"tox.group_send_message")
+ # 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,
+ c_uint(group_number),
+ c_uint32(message_type),
+ message,
+ c_size_t(len(message)),
+ # dunno
+ byref(message_id),
+ byref(error))
+
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_SEND_MESSAGE)
+ LOG_ERROR(f"group_send_message err={error.value} {s}")
+ raise ToxError(f"group_send_message err={error.value} {s}")
+
+ return bool(result)
+
+ # Group message receiving
+
+ def callback_group_message(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_message` event. Pass NULL to unset.
+ This event is triggered when the client receives a group message.
+
+ Callback: python function with params:
+ tox Tox* instance
+ group_number The group number of the group the message is intended for.
+ peer_id The ID of the peer who sent the message.
+ type The type of message (normal, action, ...).
+ message The message data.
+ length The length of the message.
+ user_data - user data
+ """
+ LOG_DEBUG(f"tox.callback_group_message")
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_message(self._tox_pointer, POINTER(None)())
+ self.group_message_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_int, c_char_p, c_size_t, c_void_p)
+ self.group_message_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_message")
+ Tox.libtoxcore.tox_callback_group_message(self._tox_pointer, self.group_message_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_message {e}")
+
+ def callback_group_private_message(self, callback: Union[Callable,None], user_data: Union[bytes,None] = None) -> None:
+ """
+ Set the callback for the `group_private_message` event. Pass NULL to unset.
+ This event is triggered when the client receives a private message.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint8, c_char_p, c_size_t, c_void_p)
+ self.group_private_message_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_private_message")
+ Tox.libtoxcore.tox_callback_group_private_message(self._tox_pointer, self.group_private_message_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_private_message {e}") # req
+
+ def callback_group_custom_packet(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_custom_packet` event. Pass NULL to unset.
+
+ This event is triggered when the client receives a custom packet.
+ """
+
+ LOG_DEBUG(f"tox.callback_group_custom_packet")
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_custom_packet(self._tox_pointer, POINTER(None)())
+ self.group_custom_packet_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, POINTER(c_uint8), c_void_p)
+ self.group_custom_packet_cb = c_callback(callback)
+ LOG_DEBUG(f"tox.callback_group_custom_packet")
+ Tox.libtoxcore.tox_callback_group_custom_packet(self._tox_pointer, self.group_custom_packet_cb)
+
+ # Group chat inviting and join/part events
+
+ def group_invite_friend(self, group_number: int, friend_number: int) -> bool:
+ """
+ Invite a friend to a group.
+
+ This function creates an invite request packet and pushes it to the send queue.
+
+ :param group_number: The group number of the group the message is intended for.
+ :param friend_number: The friend number of the friend the invite is intended for.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_invite_friend")
+ result = Tox.libtoxcore.tox_group_invite_friend(self._tox_pointer, c_uint(group_number), c_uint32(friend_number), byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_INVITE_FRIEND)
+ LOG_ERROR(f"group_invite_friend err={error.value} {s}")
+ raise ToxError(f"group_invite_friend err={error.value} {s}")
+ return bool(result)
+
+ # API change - this no longer exists
+# @staticmethod
+# def group_self_peer_info_new():
+# error = c_int()
+# f = Tox.libtoxcore.tox_group_self_peer_info_new
+# f.restype = POINTER(GroupChatSelfPeerInfo)
+# result = f(byref(error))
+# return result
+
+ # status should be dropped
+ def group_invite_accept(self, invite_data, friend_number: int, nick: str, status: str='', password=None) -> int:
+ """
+ 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.
+
+ :param invite_data: The invite data received from the `group_invite` event.
+ :param password: The password required to join the group. Set to NULL if no password is required.
+ :return the group_number on success, UINT32_MAX on failure.
+ """
+
+ error = c_int()
+ f = Tox.libtoxcore.tox_group_invite_accept
+ f.restype = c_uint32
+ if nick and type(nick) == str:
+ nick = bytes(nick, 'utf-8')
+ else:
+ nick = b''
+ if password and type(password) == str:
+ password = bytes(password, 'utf-8')
+ else:
+ password = None
+ if invite_data and type(invite_data) == str:
+ invite_data = bytes(invite_data, 'utf-8')
+ else:
+ invite_data = b''
+
+ LOG_INFO(f"group_invite_accept friend_number={friend_number} nick={nick} {invite_data}")
+ try:
+ assert type(invite_data) == bytes
+ result = f(self._tox_pointer,
+ c_uint32(friend_number),
+ invite_data,
+ c_size_t(len(invite_data)),
+ c_char_p(nick),
+ c_size_t(len(nick)),
+ c_char_p(password), len(password) if password is not None else 0,
+ byref(error))
+ except Exception as e:
+ LOG_ERROR(f"group_invite_accept ERROR {e}")
+ raise ToxError(f"group_invite_accept ERROR {e}")
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_INVITE_ACCEPT)
+ LOG_ERROR(f"group_invite_friend err={error.value} {s}")
+ raise ToxError(f"group_invite_accept {s} err={error.value}")
+ return result
+
+ def callback_group_invite(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_invite` event. Pass NULL to unset.
+
+ This event is triggered when the client receives a group invite from a friend. The client must store
+ invite_data which is used to join the group via tox_group_invite_accept.
+
+ Callback: python function with params:
+ tox - Tox*
+ friend_number The friend number of the contact who sent the invite.
+ invite_data The invite data.
+ length The length of invite_data.
+ user_data - user data
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, POINTER(None)())
+ self.group_invite_cb = None
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t,
+ POINTER(c_uint8), c_size_t, c_void_p)
+ self.group_invite_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_invite")
+ Tox.libtoxcore.tox_callback_group_invite(self._tox_pointer, self.group_invite_cb)
+ except Exception as e:
+ LOG_DEBUG(f"tox.callback_conference_invite")
+
+ def callback_group_peer_join(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_peer_join` event. Pass NULL to unset.
+
+ This event is triggered when a peer other than self joins the group.
+ Callback: python function with params:
+ tox - Tox*
+ group_number - group number
+ peer_id - peer id
+ user_data - user data
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_peer_join(self._tox_pointer, POINTER(None)())
+ self.group_peer_join_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_peer_join")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p)
+ self.group_peer_join_cb = c_callback(callback)
+ try:
+ Tox.libtoxcore.tox_callback_group_peer_join(self._tox_pointer, self.group_peer_join_cb)
+ except Exception as e:
+ LOG_ERROR(f"callback_group_peer_join {e}") # req
+
+ def callback_group_peer_exit(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_peer_exit` event. Pass NULL to unset.
+
+ This event is triggered when a peer other than self exits the group.
+ """
+
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_peer_exit(self._tox_pointer, POINTER(None)())
+ self.group_peer_exit_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_peer_exit")
+ c_callback = CFUNCTYPE(None, c_void_p,
+ c_uint32, # group_number,
+ c_uint32, # peer_id,
+ c_int, # exit_type
+ c_char_p, # name
+ c_size_t, # name length
+ c_char_p, # message
+ c_size_t, # message length
+ c_void_p) # user_data
+ self.group_peer_exit_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_peer_exit")
+ Tox.libtoxcore.tox_callback_group_peer_exit(self._tox_pointer, self.group_peer_exit_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_peer_exit {e}") # req
+ else:
+ LOG_DEBUG(f"tox.callback_group_peer_exit")
+
+ def callback_group_self_join(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_self_join` event. Pass NULL to unset.
+
+ This event is triggered when the client has successfully joined a group. Use this to initialize
+ any group information the client may need.
+ Callback: python fucntion with params:
+ tox - *Tox
+ group_number - group number
+ user_data - user data
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_self_join(self._tox_pointer, POINTER(None)())
+ self.group_self_join_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_self_join")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_void_p)
+ self.group_self_join_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_self_join")
+ Tox.libtoxcore.tox_callback_group_self_join(self._tox_pointer, self.group_self_join_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_self_join {e}") # req
+ else:
+ LOG_DEBUG(f"tox.callback_group_self_join")
+
+ def callback_group_join_fail(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_join_fail` event. Pass NULL to unset.
+
+ This event is triggered when the client fails to join a group.
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+ if callback is None:
+ Tox.libtoxcore.tox_callback_group_join_fail(self._tox_pointer, POINTER(None)())
+ self.group_join_fail_cb = None
+ return
+
+ LOG_DEBUG(f"tox.callback_group_join_fail")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_int, c_uint32, c_void_p)
+ self.group_join_fail_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_join_fail")
+ Tox.libtoxcore.tox_callback_group_join_fail(self._tox_pointer, self.group_join_fail_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_join_fail {e}") # req
+
+ # Group chat founder controls (these only work for the group founder)
+
+ def group_founder_set_password(self, group_number: int, password: str) -> bool:
+ """
+ Set or unset the group password.
+
+ This function sets the groups password, creates a new group shared state including the change,
+ and distributes it to the rest of the group.
+
+ :param group_number: The group number of the group for which we wish to set the password.
+ :param password: The password we want to set. Set password to NULL to unset the password.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_founder_set_password")
+ result = Tox.libtoxcore.tox_group_founder_set_password(self._tox_pointer, c_uint(group_number), password,
+ c_size_t(len(password)),
+ byref(error))
+ if error.value:
+ s = sGetError(error.value, TOX_ERR_GROUP_FOUNDER_SET_PASSWORD)
+ LOG_ERROR(f"group_founder_set_password err={error.value} {s}")
+ raise ToxError(f"group_founder_set_password {s} err={error.value}")
+ return bool(result)
+
+ def group_founder_set_privacy_state(self, group_number: int, privacy_state: int) -> bool:
+ """
+ Set the group privacy state.
+
+ This function sets the group's privacy state, creates a new group shared state
+ including the change, and distributes it to the rest of the group.
+
+ If an attempt is made to set the privacy state to the same state that the group is already
+ in, the function call will be successful and no action will be taken.
+
+ :param group_number: The group number of the group for which we wish to change the privacy state.
+ :param privacy_state: The privacy state we wish to set the group to.
+
+ :return true on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_founder_set_privacy_state")
+ result = Tox.libtoxcore.tox_group_founder_set_privacy_state(self._tox_pointer, c_uint(group_number), privacy_state,
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_founder_set_privacy_state err={error.value}")
+ raise ToxError(f"group_founder_set_privacy_state err={error.value}")
+ return bool(result)
+
+ def group_founder_set_peer_limit(self, group_number: int, max_peers: int) -> bool:
+ """
+ Set the group peer limit.
+
+ This function sets a limit for the number of peers who may be in the group, creates a new
+ group shared state including the change, and distributes it to the rest of the group.
+
+ :param group_number: The group number of the group for which we wish to set the peer limit.
+ :param max_peers: The maximum number of peers to allow in the group.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_founder_set_peer_limit")
+ result = Tox.libtoxcore.tox_group_founder_set_peer_limit(self._tox_pointer,
+ c_uint(group_number),
+ max_peers,
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"group_founder_set_peer_limit err={error.value}")
+ raise ToxError(f"group_founder_set_peer_limit err={error.value}")
+ return bool(result)
+
+ # Group chat moderation
+
+ def group_mod_set_role(self, group_number: int, peer_id: int, role: int) -> bool:
+ """
+ Set a peer's role.
+
+ This function will first remove the peer's previous role and then assign them a new role.
+ It will also send a packet to the rest of the group, requesting that they perform
+ the role reassignment. Note: peers cannot be set to the founder role.
+
+ :param group_number: The group number of the group the in which you wish set the peer's role.
+ :param peer_id: The ID of the peer whose role you wish to set.
+ :param role: The role you wish to set the peer to.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_mod_set_role")
+ result = Tox.libtoxcore.tox_group_mod_set_role(self._tox_pointer,
+ c_uint(group_number),
+ c_uint32(peer_id),
+ c_uint32(role), byref(error))
+ if error.value:
+ LOG_ERROR(f"group_mod_set_role err={error.value}")
+ raise ToxError(f"group_mod_set_role err={error.value}")
+ return bool(result)
+
+ def callback_group_moderation(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `group_moderation` event. Pass NULL to unset.
+
+ This event is triggered when a moderator or founder executes a moderation event.
+ (tox_data->tox, group_number, source_peer_number, target_peer_number,
+ (Tox_Group_Mod_Event)mod_type, tox_data->user_data);
+ TOX_GROUP_MOD_EVENT = [0,1,2,3,4] TOX_GROUP_MOD_EVENT['MODERATOR']
+ """
+ if user_data is not None:
+ isinstance(user_data, Array), type(user_data)
+
+# LOG_DEBUG(f"callback_group_moderation")
+ if callback is None:
+ self.group_moderation_cb = None
+ LOG_DEBUG(f"tox.callback_group_moderation")
+ Tox.libtoxcore.tox_callback_group_moderation(self._tox_pointer, POINTER(None)())
+ return
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint32, c_int, c_void_p)
+ self.group_moderation_cb = c_callback(callback)
+ try:
+ LOG_DEBUG(f"tox.callback_group_moderation")
+ Tox.libtoxcore.tox_callback_group_moderation(self._tox_pointer, self.group_moderation_cb)
+ except Exception as e:
+ LOG_ERROR(f"tox.callback_group_moderation {e}") # req
+ else:
+ LOG_DEBUG(f"tox.callback_group_moderation")
+
+ def group_toggle_set_ignore(self, group_number: int, peer_id: int, ignore) -> bool:
+ return self.group_set_ignore(group_number, peer_id, ignore)
+
+ def group_set_ignore(self, group_number: int, peer_id: int, ignore: bool) -> bool:
+ """
+ Ignore or unignore a peer.
+
+ :param group_number: The group number of the group the in which you wish to ignore a peer.
+ :param peer_id: The ID of the peer who shall be ignored or unignored.
+ :param ignore: True to ignore the peer, false to unignore the peer.
+
+ :return True on success.
+ """
+ if group_number < 0:
+ raise ToxError(f"tox_group_ group_number < 0 {group_number}")
+
+ error = c_int()
+ LOG_DEBUG(f"tox.group_set_ignore")
+ result = Tox.libtoxcore.tox_group_set_ignore(self._tox_pointer,
+ c_uint32(group_number),
+ c_uint32(peer_id),
+ c_bool(ignore),
+ byref(error))
+ if error.value:
+ LOG_ERROR(f"tox.group_set_ignore err={error.value}")
+ raise ToxError("tox_group_set_ignore err={error.value}")
+ return bool(result)
diff --git a/tox_wrapper/toxav.py b/tox_wrapper/toxav.py
new file mode 100644
index 0000000..dfe9ec8
--- /dev/null
+++ b/tox_wrapper/toxav.py
@@ -0,0 +1,409 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+from ctypes import (CFUNCTYPE, POINTER, ArgumentError, byref, c_bool, c_char_p,
+ c_int, c_int32, c_size_t, c_uint8, c_uint16, c_uint32,
+ c_void_p, cast)
+from typing import Union, Callable
+
+try:
+ from tox_wrapper.libtox import LibToxAV
+ import tox_wrapper.toxav_enums as enum
+except:
+ from libtox import LibToxAV
+ import toxav_enums as enum
+class ToxError(RuntimeError): pass
+
+def LOG_ERROR(a: str) -> None: print('EROR> '+a)
+def LOG_WARN(a: str) -> None: print('WARN> '+a)
+def LOG_INFO(a: str) -> None: print('INFO> '+a)
+def LOG_DEBUG(a: str) -> None: print('DBUG> '+a)
+def LOG_TRACE(a: str) -> None: pass # print('DEBUGx: '+a)
+
+class ToxAV:
+ """
+ The ToxAV instance type. Each ToxAV instance can be bound to only one Tox instance, and Tox instance can have only
+ one ToxAV instance. One must make sure to close ToxAV instance prior closing Tox instance otherwise undefined
+ behaviour occurs. Upon closing of ToxAV instance, all active calls will be forcibly terminated without notifying
+ peers.
+ """
+
+ # Creation and destruction
+
+ def __init__(self, tox_pointer):
+ """
+ Start new A/V session. There can only be only one session per Tox instance.
+
+ :param tox_pointer: pointer to Tox instance
+ """
+ self.libtoxav = LibToxAV()
+ toxav_err_new = c_int()
+ f = self.libtoxav.toxav_new
+ f.restype = POINTER(c_void_p)
+ self._toxav_pointer = f(tox_pointer, byref(toxav_err_new))
+ toxav_err_new = toxav_err_new.value
+ if toxav_err_new == enum.TOXAV_ERR_NEW['NULL']:
+ raise ArgumentError('One of the arguments to the function was NULL when it was not expected.')
+ if toxav_err_new == enum.TOXAV_ERR_NEW['MALLOC']:
+ raise MemoryError('Memory allocation failure while trying to allocate structures required for the A/V '
+ 'session.')
+ if toxav_err_new == enum.TOXAV_ERR_NEW['MULTIPLE']:
+ raise RuntimeError('Attempted to create a second session for the same Tox instance.')
+
+ self.call_state_cb = None
+ self.audio_receive_frame_cb = None
+ self.video_receive_frame_cb = None
+ self.call_cb = None
+
+ def kill(self) -> None:
+ """
+ Releases all resources associated with the A/V session.
+
+ If any calls were ongoing, these will be forcibly terminated without notifying peers. After calling this
+ function, no other functions may be called and the av pointer becomes invalid.
+ """
+ self.libtoxav.toxav_kill(self._toxav_pointer)
+
+ def get_tox_pointer(self):
+ """
+ Returns the Tox instance the A/V object was created for.
+
+ :return: pointer to the Tox instance
+ """
+ self.libtoxav.toxav_get_tox.restype = POINTER(c_void_p)
+ return self.libtoxav.toxav_get_tox(self._toxav_pointer)
+
+ # A/V event loop
+
+ def iteration_interval(self) -> int:
+ """
+ Returns the interval in milliseconds when the next toxav_iterate call should be. If no call is active at the
+ moment, this function returns 200.
+
+ :return: interval in milliseconds
+ """
+ return int(self.libtoxav.toxav_iteration_interval(self._toxav_pointer))
+
+ def iterate(self) -> None:
+ """
+ Main loop for the session. This function needs to be called in intervals of toxav_iteration_interval()
+ milliseconds. It is best called in the separate thread from tox_iterate.
+ """
+ self.libtoxav.toxav_iterate(self._toxav_pointer)
+
+ # Call setup
+
+ def call(self, friend_number: int, audio_bit_rate: int, video_bit_rate: int) -> bool:
+ """
+ Call a friend. This will start ringing the friend.
+
+ It is the client's responsibility to stop ringing after a certain timeout, if such behaviour is desired. If the
+ client does not stop ringing, the library will not stop until the friend is disconnected. Audio and video
+ receiving are both enabled by default.
+
+ :param friend_number: The friend number of the friend that should be called.
+ :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending.
+ :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending.
+ :return: True on success.
+ """
+ toxav_err_call = c_int()
+ LOG_DEBUG(f"toxav_call")
+ result = self.libtoxav.toxav_call(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate),
+ c_uint32(video_bit_rate), byref(toxav_err_call))
+ toxav_err_call = toxav_err_call.value
+ if toxav_err_call == enum.TOXAV_ERR_CALL['OK']:
+ return bool(result)
+ if toxav_err_call == enum.TOXAV_ERR_CALL['MALLOC']:
+ raise MemoryError('A resource allocation error occurred while trying to create the structures required for '
+ 'the call.')
+ if toxav_err_call == enum.TOXAV_ERR_CALL['SYNC']:
+ raise RuntimeError('Synchronization error occurred.')
+ if toxav_err_call == enum.TOXAV_ERR_CALL['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ if toxav_err_call == enum.TOXAV_ERR_CALL['FRIEND_NOT_CONNECTED']:
+ raise ArgumentError('The friend was valid, but not currently connected.')
+ if toxav_err_call == enum.TOXAV_ERR_CALL['FRIEND_ALREADY_IN_CALL']:
+ raise ArgumentError('Attempted to call a friend while already in an audio or video call with them.')
+ if toxav_err_call == enum.TOXAV_ERR_CALL['INVALID_BIT_RATE']:
+ raise ArgumentError('Audio or video bit rate is invalid.')
+ raise ArgumentError('The function did not return OK')
+
+ def callback_call(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `call` event. Pass None to unset.
+
+ :param callback: The function for the call callback.
+
+ Should take pointer (c_void_p) to ToxAV object,
+ The friend number (c_uint32) from which the call is incoming.
+ True (c_bool) if friend is sending audio.
+ True (c_bool) if friend is sending video.
+ pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ if callback is None:
+ self.libtoxav.toxav_callback_call(self._toxav_pointer, POINTER(None)(), user_data)
+ self.call_cb = None
+ return
+ LOG_DEBUG(f"toxav_callback_call")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_bool, c_void_p)
+ self.call_cb = c_callback(callback)
+ self.libtoxav.toxav_callback_call(self._toxav_pointer, self.call_cb, user_data)
+
+ def answer(self, friend_number: int, audio_bit_rate: int, video_bit_rate: int) -> bool:
+ """
+ Accept an incoming call.
+
+ If answering fails for any reason, the call will still be pending and it is possible to try and answer it later.
+ Audio and video receiving are both enabled by default.
+
+ :param friend_number: The friend number of the friend that is calling.
+ :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending.
+ :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending.
+ :return: True on success.
+ """
+ toxav_err_answer = c_int()
+ LOG_DEBUG(f"toxav_answer")
+ result = self.libtoxav.toxav_answer(self._toxav_pointer,
+ c_uint32(friend_number),
+ c_uint32(audio_bit_rate),
+ c_uint32(video_bit_rate),
+ byref(toxav_err_answer))
+ toxav_err_answer = toxav_err_answer.value
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['OK']:
+ return bool(result)
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['SYNC']:
+ raise RuntimeError('Synchronization error occurred.')
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['CODEC_INITIALIZATION']:
+ raise RuntimeError('Failed to initialize codecs for call session. Note that codec initiation will fail if '
+ 'there is no receive callback registered for either audio or video.')
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend number did not designate a valid friend.')
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['FRIEND_NOT_CALLING']:
+ raise ArgumentError('The friend was valid, but they are not currently trying to initiate a call. This is '
+ 'also returned if this client is already in a call with the friend.')
+ if toxav_err_answer == enum.TOXAV_ERR_ANSWER['INVALID_BIT_RATE']:
+ raise ArgumentError('Audio or video bit rate is invalid.')
+ raise ToxError('The function did not return OK')
+
+ # Call state graph
+
+ def callback_call_state(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `call_state` event. Pass None to unset.
+
+ :param callback: Python function.
+ The function for the call_state callback.
+
+ Should take pointer (c_void_p) to ToxAV object,
+ The friend number (c_uint32) for which the call state changed.
+ The bitmask of the new call state which is guaranteed to be different than the previous state. The state is set
+ to 0 when the call is paused. The bitmask represents all the activities currently performed by the friend.
+ pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ if callback is None:
+ self.libtoxav.toxav_callback_call_state(self._toxav_pointer, POINTER(None)(), user_data)
+ self.call_state_cb = None
+ return
+ LOG_DEBUG(f"callback_call_state")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p)
+ self.call_state_cb = c_callback(callback)
+ self.libtoxav.toxav_callback_call_state(self._toxav_pointer, self.call_state_cb, user_data)
+
+ # Call control
+
+ def call_control(self, friend_number: int, control: int) -> bool:
+ """
+ Sends a call control command to a friend.
+
+ :param friend_number: The friend number of the friend this client is in a call with.
+ :param control: The control command to send.
+ :return: True on success.
+ """
+ toxav_err_call_control = c_int()
+ LOG_DEBUG(f"call_control")
+ result = self.libtoxav.toxav_call_control(self._toxav_pointer,
+ c_uint32(friend_number),
+ c_int(control),
+ byref(toxav_err_call_control))
+ toxav_err_call_control = toxav_err_call_control.value
+ if toxav_err_call_control == enum.TOXAV_ERR_CALL_CONTROL['OK']:
+ return bool(result)
+ if toxav_err_call_control == enum.TOXAV_ERR_CALL_CONTROL['SYNC']:
+ raise RuntimeError('Synchronization error occurred.')
+ if toxav_err_call_control == enum.TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if toxav_err_call_control == enum.TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_IN_CALL']:
+ raise RuntimeError('This client is currently not in a call with the friend. Before the call is answered, '
+ 'only CANCEL is a valid control.')
+ if toxav_err_call_control == enum.TOXAV_ERR_CALL_CONTROL['INVALID_TRANSITION']:
+ raise RuntimeError('Happens if user tried to pause an already paused call or if trying to resume a call '
+ 'that is not paused.')
+ raise ToxError('The function did not return OK.')
+
+ # TODO Controlling bit rates
+
+ # A/V sending
+
+ def audio_send_frame(self, friend_number: int, pcm, sample_count: int, channels: int, sampling_rate: int) -> bool:
+ """
+ Send an audio frame to a friend.
+
+ The expected format of the PCM data is: [s1c1][s1c2][...][s2c1][s2c2][...]...
+ Meaning: sample 1 for channel 1, sample 1 for channel 2, ...
+ For mono audio, this has no meaning, every sample is subsequent. For stereo, this means the expected format is
+ LRLRLR... with samples for left and right alternating.
+
+ :param friend_number: The friend number of the friend to which to send an audio frame.
+ :param pcm: An array of audio samples. The size of this array must be sample_count * channels.
+ :param sample_count: Number of samples in this frame. Valid numbers here are
+ ((sample rate) * (audio length) / 1000), where audio length can be 2.5, 5, 10, 20, 40 or 60 milliseconds.
+ :param channels: Number of audio channels. Sulpported values are 1 and 2.
+ :param sampling_rate: Audio sampling rate used in this frame. Valid sampling rates are 8000, 12000, 16000,
+ 24000, or 48000.
+ """
+ toxav_err_send_frame = c_int()
+ LOG_TRACE(f"toxav_audio_send_frame")
+ assert sampling_rate in [8000, 12000, 16000, 24000, 48000]
+ result = self.libtoxav.toxav_audio_send_frame(self._toxav_pointer,
+ c_uint32(friend_number),
+ cast(pcm, c_void_p),
+ c_size_t(sample_count), c_uint8(channels),
+ c_uint32(sampling_rate), byref(toxav_err_send_frame))
+ toxav_err_send_frame = toxav_err_send_frame.value
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['OK']:
+ return bool(result)
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['NULL']:
+ raise ArgumentError('The samples data pointer was NULL.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']:
+ raise RuntimeError('This client is currently not in a call with the friend.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['SYNC']:
+ raise RuntimeError('Synchronization error occurred.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['INVALID']:
+ raise ArgumentError('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.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']:
+ raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said'
+ 'payload.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['RTP_FAILED']:
+ RuntimeError('Failed to push frame through rtp interface.')
+ raise ToxError('The function did not return OK.')
+
+ def video_send_frame(self, friend_number: int, width: int, height: int, y, u, v) -> bool:
+ """
+ Send a video frame to a friend.
+
+ Y - plane should be of size: height * width
+ U - plane should be of size: (height/2) * (width/2)
+ V - plane should be of size: (height/2) * (width/2)
+
+ :param friend_number: The friend number of the friend to which to send a video frame.
+ :param width: Width of the frame in pixels.
+ :param height: Height of the frame in pixels.
+ :param y: Y (Luminance) plane data.
+ :param u: U (Chroma) plane data.
+ :param v: V (Chroma) plane data.
+ """
+ toxav_err_send_frame = c_int()
+ LOG_TRACE(f"toxav_video_send_frame")
+ result = self.libtoxav.toxav_video_send_frame(self._toxav_pointer,
+ c_uint32(friend_number),
+ c_uint16(width),
+ c_uint16(height),
+ c_char_p(y),
+ c_char_p(u),
+ c_char_p(v),
+ byref(toxav_err_send_frame))
+ toxav_err_send_frame = toxav_err_send_frame.value
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['OK']:
+ return bool(result)
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['NULL']:
+ raise ArgumentError('One of Y, U, or V was NULL.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']:
+ raise ArgumentError('The friend_number passed did not designate a valid friend.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']:
+ raise RuntimeError('This client is currently not in a call with the friend.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['SYNC']:
+ raise RuntimeError('Synchronization error occurred.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['INVALID']:
+ raise ArgumentError('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.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']:
+ raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said'
+ 'payload.')
+ if toxav_err_send_frame == enum.TOXAV_ERR_SEND_FRAME['RTP_FAILED']:
+ RuntimeError('Failed to push frame through rtp interface.')
+ raise ToxError('The function did not return OK.')
+
+ # A/V receiving
+
+ def callback_audio_receive_frame(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `audio_receive_frame` event. Pass None to unset.
+
+ :param callback: Python function.
+ Function for the audio_receive_frame callback. The callback can be called multiple times per single
+ iteration depending on the amount of queued frames in the buffer. The received format is the same as in send
+ function.
+
+ Should take pointer (c_void_p) to ToxAV object,
+ The friend number (c_uint32) of the friend who sent an audio frame.
+ An array (c_uint8) of audio samples (sample_count * channels elements).
+ The number (c_size_t) of audio samples per channel in the PCM array.
+ Number (c_uint8) of audio channels.
+ Sampling rate (c_uint32) used in this frame.
+ pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ if callback is None:
+ self.libtoxav.toxav_callback_audio_receive_frame(self._toxav_pointer,
+ POINTER(None)(),
+ user_data)
+ self.audio_receive_frame_cb = None
+ return
+ LOG_DEBUG(f"toxav_callback_audio_receive_frame")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_uint8, c_uint32, c_void_p)
+ self.audio_receive_frame_cb = c_callback(callback)
+ self.libtoxav.toxav_callback_audio_receive_frame(self._toxav_pointer, self.audio_receive_frame_cb, user_data)
+
+ def callback_video_receive_frame(self, callback: Union[Callable,None], user_data) -> None:
+ """
+ Set the callback for the `video_receive_frame` event. Pass None to unset.
+
+ :param callback: Python function.
+ The function type for the video_receive_frame callback.
+
+ Should take
+ toxAV pointer (c_void_p) to ToxAV object,
+ friend_number The friend number (c_uint32) of the friend who sent a video frame.
+ width Width (c_uint16) of the frame in pixels.
+ height Height (c_uint16) of the frame in pixels.
+ y
+ u
+ v Plane data (POINTER(c_uint8)).
+ The size of plane data is derived from width and height where
+ Y = MAX(width, abs(ystride)) * height,
+ U = MAX(width/2, abs(ustride)) * (height/2) and
+ V = MAX(width/2, abs(vstride)) * (height/2).
+ ystride
+ ustride
+ vstride Strides data (c_int32). Strides represent padding for each plane that may or may not be present. You must
+ handle strides in your image processing code. Strides are negative if the image is bottom-up
+ hence why you MUST abs() it when calculating plane buffer size.
+ user_data pointer (c_void_p) to user_data
+ :param user_data: pointer (c_void_p) to user data
+ """
+ if callback is None:
+ self.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, POINTER(None)(), user_data)
+ self.video_receive_frame_cb = None
+ return
+
+ LOG_DEBUG(f"toxav_callback_video_receive_frame")
+ c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint16, c_uint16,
+ POINTER(c_uint8), POINTER(c_uint8), POINTER(c_uint8),
+ c_int32, c_int32, c_int32,
+ c_void_p)
+ self.video_receive_frame_cb = c_callback(callback)
+ self.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, self.video_receive_frame_cb, user_data)
diff --git a/tox_wrapper/toxav_enums.py b/tox_wrapper/toxav_enums.py
new file mode 100644
index 0000000..f8817e1
--- /dev/null
+++ b/tox_wrapper/toxav_enums.py
@@ -0,0 +1,133 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+TOXAV_ERR_NEW = {
+ # The function returned successfully.
+ 'OK': 0,
+ # One of the arguments to the function was NULL when it was not expected.
+ 'NULL': 1,
+ # Memory allocation failure while trying to allocate structures required for the A/V session.
+ 'MALLOC': 2,
+ # Attempted to create a second session for the same Tox instance.
+ 'MULTIPLE': 3,
+}
+
+TOXAV_ERR_CALL = {
+ # The function returned successfully.
+ 'OK': 0,
+ # A resource allocation error occurred while trying to create the structures required for the call.
+ 'MALLOC': 1,
+ # Synchronization error occurred.
+ 'SYNC': 2,
+ # The friend number did not designate a valid friend.
+ 'FRIEND_NOT_FOUND': 3,
+ # The friend was valid, but not currently connected.
+ 'FRIEND_NOT_CONNECTED': 4,
+ # Attempted to call a friend while already in an audio or video call with them.
+ 'FRIEND_ALREADY_IN_CALL': 5,
+ # Audio or video bit rate is invalid.
+ 'INVALID_BIT_RATE': 6,
+}
+
+TOXAV_ERR_ANSWER = {
+ # The function returned successfully.
+ 'OK': 0,
+ # Synchronization error occurred.
+ 'SYNC': 1,
+ # Failed to initialize codecs for call session. Note that codec initiation will fail if there is no receive callback
+ # registered for either audio or video.
+ 'CODEC_INITIALIZATION': 2,
+ # The friend number did not designate a valid friend.
+ 'FRIEND_NOT_FOUND': 3,
+ # The friend was valid, but they are not currently trying to initiate a call. This is also returned if this client
+ # is already in a call with the friend.
+ 'FRIEND_NOT_CALLING': 4,
+ # Audio or video bit rate is invalid.
+ 'INVALID_BIT_RATE': 5,
+}
+
+TOXAV_FRIEND_CALL_STATE = {
+ # Set by the AV core if an error occurred on the remote end or if friend timed out. This is the final state after
+ # which no more state transitions can occur for the call. This call state will never be triggered in combination
+ # with other call states.
+ 'ERROR': 1,
+ # The call has finished. This is the final state after which no more state transitions can occur for the call. This
+ # call state will never be triggered in combination with other call states.
+ 'FINISHED': 2,
+ # The flag that marks that friend is sending audio.
+ 'SENDING_A': 4,
+ # The flag that marks that friend is sending video.
+ 'SENDING_V': 8,
+ # The flag that marks that friend is receiving audio.
+ 'ACCEPTING_A': 16,
+ # The flag that marks that friend is receiving video.
+ 'ACCEPTING_V': 32,
+}
+
+TOXAV_CALL_CONTROL = {
+ # Resume a previously paused call. Only valid if the pause was caused by this client, if not, this control is
+ # ignored. Not valid before the call is accepted.
+ 'RESUME': 0,
+ # Put a call on hold. Not valid before the call is accepted.
+ 'PAUSE': 1,
+ # Reject a call if it was not answered, yet. Cancel a call after it was answered.
+ 'CANCEL': 2,
+ # Request that the friend stops sending audio. Regardless of the friend's compliance, this will cause the
+ # audio_receive_frame event to stop being triggered on receiving an audio frame from the friend.
+ 'MUTE_AUDIO': 3,
+ # Calling this control will notify client to start sending audio again.
+ 'UNMUTE_AUDIO': 4,
+ # Request that the friend stops sending video. Regardless of the friend's compliance, this will cause the
+ # video_receive_frame event to stop being triggered on receiving a video frame from the friend.
+ 'HIDE_VIDEO': 5,
+ # Calling this control will notify client to start sending video again.
+ 'SHOW_VIDEO': 6,
+}
+
+TOXAV_ERR_CALL_CONTROL = {
+ # The function returned successfully.
+ 'OK': 0,
+ # Synchronization error occurred.
+ 'SYNC': 1,
+ # The friend_number passed did not designate a valid friend.
+ 'FRIEND_NOT_FOUND': 2,
+ # This client is currently not in a call with the friend. Before the call is answered, only CANCEL is a valid
+ # control.
+ 'FRIEND_NOT_IN_CALL': 3,
+ # Happens if user tried to pause an already paused call or if trying to resume a call that is not paused.
+ 'INVALID_TRANSITION': 4,
+}
+
+TOXAV_ERR_BIT_RATE_SET = {
+ # The function returned successfully.
+ 'OK': 0,
+ # Synchronization error occurred.
+ 'SYNC': 1,
+ # The audio bit rate passed was not one of the supported values.
+ 'INVALID_AUDIO_BIT_RATE': 2,
+ # The video bit rate passed was not one of the supported values.
+ 'INVALID_VIDEO_BIT_RATE': 3,
+ # The friend_number passed did not designate a valid friend.
+ 'FRIEND_NOT_FOUND': 4,
+ # This client is currently not in a call with the friend.
+ 'FRIEND_NOT_IN_CALL': 5,
+}
+
+TOXAV_ERR_SEND_FRAME = {
+ # The function returned successfully.
+ 'OK': 0,
+ # In case of video, one of Y, U, or V was NULL. In case of audio, the samples data pointer was NULL.
+ 'NULL': 1,
+ # The friend_number passed did not designate a valid friend.
+ 'FRIEND_NOT_FOUND': 2,
+ # This client is currently not in a call with the friend.
+ 'FRIEND_NOT_IN_CALL': 3,
+ # Synchronization error occurred.
+ 'SYNC': 4,
+ # 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.
+ 'INVALID': 5,
+ # Either friend turned off audio or video receiving or we turned off sending for the said payload.
+ 'PAYLOAD_TYPE_DISABLED': 6,
+ # Failed to push frame through rtp interface.
+ 'RTP_FAILED': 7,
+}
diff --git a/tox_wrapper/toxcore_enums_and_consts.py b/tox_wrapper/toxcore_enums_and_consts.py
new file mode 100644
index 0000000..ed53861
--- /dev/null
+++ b/tox_wrapper/toxcore_enums_and_consts.py
@@ -0,0 +1,982 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+TOX_USER_STATUS = {
+ 'NONE': 0,
+ 'AWAY': 1,
+ 'BUSY': 2,
+}
+
+TOX_MESSAGE_TYPE = {
+ 'NORMAL': 0,
+ 'ACTION': 1,
+}
+
+TOX_PROXY_TYPE = {
+ 'NONE': 0,
+ 'HTTP': 1,
+ 'SOCKS5': 2,
+}
+
+TOX_SAVEDATA_TYPE = {
+ 'NONE': 0,
+ 'TOX_SAVE': 1,
+ 'SECRET_KEY': 2,
+}
+
+TOX_ERR_OPTIONS_NEW = {
+ 'OK': 0,
+ 'MALLOC': 1,
+}
+
+TOX_ERR_NEW = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'MALLOC': 2,
+ 'PORT_ALLOC': 3,
+ 'PROXY_BAD_TYPE': 4,
+ 'PROXY_BAD_HOST': 5,
+ 'PROXY_BAD_PORT': 6,
+ 'PROXY_NOT_FOUND': 7,
+ 'LOAD_ENCRYPTED': 8,
+ 'LOAD_BAD_FORMAT': 9,
+ 'TCP_SERVER_ALLOC': 10,
+}
+
+TOX_ERR_BOOTSTRAP = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'BAD_HOST': 2,
+ 'BAD_PORT': 3,
+}
+
+TOX_CONNECTION = {
+ 'NONE': 0,
+ 'TCP': 1,
+ 'UDP': 2,
+}
+
+TOX_ERR_SET_INFO = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'TOO_LONG': 2,
+ # The function returned successfully.
+ 'TOX_ERR_SET_INFO_OK': 0,
+ # One of the arguments to the function was NULL when it was not expected.
+ 'TOX_ERR_SET_INFO_NULL': 1,
+ # Information length exceeded maximum permissible size.
+ 'TOX_ERR_SET_INFO_TOO_LONG': 2,
+}
+
+
+TOX_ERR_FRIEND_ADD = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'TOO_LONG': 2,
+ 'NO_MESSAGE': 3,
+ 'OWN_KEY': 4,
+ 'ALREADY_SENT': 5,
+ 'BAD_CHECKSUM': 6,
+ 'SET_NEW_NOSPAM': 7,
+ 'MALLOC': 8,
+}
+
+TOX_ERR_FRIEND_DELETE = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+}
+
+TOX_ERR_FRIEND_BY_PUBLIC_KEY = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'NOT_FOUND': 2,
+}
+
+TOX_ERR_FRIEND_GET_PUBLIC_KEY = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+}
+
+TOX_ERR_FRIEND_GET_LAST_ONLINE = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+}
+
+TOX_ERR_FRIEND_QUERY = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+}
+
+TOX_ERR_SET_TYPING = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+}
+
+TOX_ERR_FRIEND_SEND_MESSAGE = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+ 'FRIEND_NOT_CONNECTED': 3,
+ 'SENDQ': 4,
+ 'TOO_LONG': 5,
+ 'EMPTY': 6,
+}
+
+TOX_FILE_KIND = {
+ 'DATA': 0,
+ 'AVATAR': 1,
+}
+
+TOX_FILE_CONTROL = {
+ 'RESUME': 0,
+ 'PAUSE': 1,
+ 'CANCEL': 2,
+}
+
+TOX_ERR_FILE_CONTROL = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+ 'FRIEND_NOT_CONNECTED': 2,
+ 'NOT_FOUND': 3,
+ 'NOT_PAUSED': 4,
+ 'DENIED': 5,
+ 'ALREADY_PAUSED': 6,
+ 'SENDQ': 7,
+}
+
+TOX_ERR_FILE_SEEK = {
+ 'OK': 0,
+ 'FRIEND_NOT_FOUND': 1,
+ 'FRIEND_NOT_CONNECTED': 2,
+ 'NOT_FOUND': 3,
+ 'DENIED': 4,
+ 'INVALID_POSITION': 5,
+ 'SENDQ': 6,
+}
+
+TOX_ERR_FILE_GET = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+ 'NOT_FOUND': 3,
+}
+
+TOX_ERR_FILE_SEND = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+ 'FRIEND_NOT_CONNECTED': 3,
+ 'NAME_TOO_LONG': 4,
+ 'TOO_MANY': 5,
+}
+
+TOX_ERR_FILE_SEND_CHUNK = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+ 'FRIEND_NOT_CONNECTED': 3,
+ 'NOT_FOUND': 4,
+ 'NOT_TRANSFERRING': 5,
+ 'INVALID_LENGTH': 6,
+ 'SENDQ': 7,
+ 'WRONG_POSITION': 8,
+}
+
+TOX_ERR_FRIEND_CUSTOM_PACKET = {
+ 'OK': 0,
+ 'NULL': 1,
+ 'FRIEND_NOT_FOUND': 2,
+ 'FRIEND_NOT_CONNECTED': 3,
+ 'INVALID': 4,
+ 'EMPTY': 5,
+ 'TOO_LONG': 6,
+ 'SENDQ': 7,
+}
+
+TOX_ERR_GET_PORT = {
+ 'OK': 0,
+ 'NOT_BOUND': 1,
+}
+
+TOX_GROUP_PRIVACY_STATE = {
+
+ #
+ # The group is considered to be public. Anyone may join the group using the Chat ID.
+ #
+ # If the group is in this state, even if the Chat ID is never explicitly shared
+ # with someone outside of the group, information including the Chat ID, IP addresses,
+ # and peer ID's (but not Tox ID's) is visible to anyone with access to a node
+ # storing a DHT entry for the given group.
+ #
+ 'PUBLIC': 0,
+
+ #
+ # The group is considered to be private. The only way to join the group is by having
+ # someone in your contact list send you an invite.
+ #
+ # If the group is in this state, no group information (mentioned above) is present in the DHT;
+ # the DHT is not used for any purpose at all. If a public group is set to private,
+ # all DHT information related to the group will expire shortly.
+ #
+ 'PRIVATE': 1
+}
+
+TOX_GROUP_ROLE = {
+
+ #
+ # May kick and ban all other peers as well as set their role to anything (except founder).
+ # Founders may also set the group password, toggle the privacy state, and set the peer limit.
+ #
+ 'FOUNDER': 0,
+
+ #
+ # May kick, ban and set the user and observer roles for peers below this role.
+ # May also set the group topic.
+ #
+ 'MODERATOR': 1,
+
+ #
+ # May communicate with other peers normally.
+ #
+ 'USER': 2,
+
+ #
+ # May observe the group and ignore peers; may not communicate with other peers or with the group.
+ #
+ 'OBSERVER': 3
+}
+
+TOX_ERR_GROUP_NEW = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_NEW_OK': 0,
+
+ #
+ # The group name exceeded TOX_GROUP_MAX_GROUP_NAME_LENGTH.
+ #
+ 'TOX_ERR_GROUP_NEW_TOO_LONG': 1,
+
+ #
+ # group_name is NULL or length is zero.
+ #
+ 'TOX_ERR_GROUP_NEW_EMPTY': 2,
+
+ #
+ # TOX_GROUP_PRIVACY_STATE is an invalid type.
+ #
+ 'TOX_ERR_GROUP_NEW_PRIVACY': 3,
+
+ #
+ # The group instance failed to initialize.
+ #
+ 'TOX_ERR_GROUP_NEW_INIT': 4,
+
+ #
+ # The group state failed to initialize. This usually indicates that something went wrong
+ # related to cryptographic signing.
+ #
+ 'TOX_ERR_GROUP_NEW_STATE': 5,
+
+ #
+ # The group failed to announce to the DHT. This indicates a network related error.
+ #
+ 'TOX_ERR_GROUP_NEW_ANNOUNCE': 6,
+}
+
+TOX_ERR_GROUP_JOIN = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_JOIN_OK': 0,
+
+ #
+ # The group instance failed to initialize.
+ #
+ 'TOX_ERR_GROUP_JOIN_INIT': 1,
+
+ #
+ # The chat_id pointer is set to NULL or a group with chat_id already exists. This usually
+ # happens if the client attempts to create multiple sessions for the same group.
+ #
+ 'TOX_ERR_GROUP_JOIN_BAD_CHAT_ID': 2,
+
+ #
+ # Password length exceeded TOX_GROUP_MAX_PASSWORD_SIZE.
+ #
+ 'TOX_ERR_GROUP_JOIN_TOO_LONG': 3,
+}
+
+TOX_ERR_GROUP_IS_CONNECTED = {
+ 'TOX_ERR_GROUP_IS_CONNECTED_OK': 0,
+ 'TOX_ERR_GROUP_IS_CONNECTED_GROUP_NOT_FOUND': 1
+}
+
+TOX_ERR_GROUP_RECONNECT = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_RECONNECT_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_RECONNECT_GROUP_NOT_FOUND': 1,
+}
+
+TOX_ERR_GROUP_DISCONNECT = {
+
+ # The function returned successfully.
+ 'TOX_ERR_GROUP_DISCONNECT_OK': 0,
+
+ # The group number passed did not designate a valid group.
+ 'TOX_ERR_GROUP_DISCONNECT_GROUP_NOT_FOUND': 1,
+
+ # The group is already disconnected.
+ 'TOX_ERR_GROUP_DISCONNECT_ALREADY_DISCONNECTED': 2,
+}
+
+
+TOX_ERR_GROUP_LEAVE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_LEAVE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_LEAVE_GROUP_NOT_FOUND': 1,
+
+ #
+ # Message length exceeded 'TOX_GROUP_MAX_PART_LENGTH.
+ #
+ 'TOX_ERR_GROUP_LEAVE_TOO_LONG': 2,
+
+ #
+ # The parting packet failed to send.
+ #
+ 'TOX_ERR_GROUP_LEAVE_FAIL_SEND': 3,
+
+ #
+ # The group chat instance failed to be deleted. This may occur due to memory related errors.
+ #
+ 'TOX_ERR_GROUP_LEAVE_DELETE_FAIL': 4,
+}
+
+TOX_ERR_GROUP_SELF_QUERY = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SELF_QUERY_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SELF_QUERY_GROUP_NOT_FOUND': 1,
+}
+
+
+TOX_ERR_GROUP_SELF_NAME_SET = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_GROUP_NOT_FOUND': 1,
+
+ #
+ # Name length exceeded 'TOX_MAX_NAME_LENGTH.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_TOO_LONG': 2,
+
+ #
+ # The length given to the set function is zero or name is a NULL pointer.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_INVALID': 3,
+
+ #
+ # The name is already taken by another peer in the group.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_TAKEN': 4,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_SELF_NAME_SET_FAIL_SEND': 5
+}
+
+TOX_ERR_GROUP_SELF_STATUS_SET = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SELF_STATUS_SET_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SELF_STATUS_SET_GROUP_NOT_FOUND': 1,
+
+ #
+ # An invalid type was passed to the set function.
+ #
+ 'TOX_ERR_GROUP_SELF_STATUS_SET_INVALID': 2,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_SELF_STATUS_SET_FAIL_SEND': 3
+}
+
+TOX_ERR_GROUP_PEER_QUERY = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_PEER_QUERY_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_PEER_QUERY_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ID passed did not designate a valid peer.
+ #
+ 'TOX_ERR_GROUP_PEER_QUERY_PEER_NOT_FOUND': 2
+}
+
+TOX_ERR_GROUP_STATE_QUERIES = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_STATE_QUERIES_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_STATE_QUERIES_GROUP_NOT_FOUND': 1
+}
+
+
+TOX_ERR_GROUP_TOPIC_SET = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_GROUP_NOT_FOUND': 1,
+
+ #
+ # Topic length exceeded 'TOX_GROUP_MAX_TOPIC_LENGTH.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_TOO_LONG': 2,
+
+ #
+ # The caller does not have the required permissions to set the topic.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_PERMISSIONS': 3,
+
+ #
+ # The packet could not be created. This error is usually related to cryptographic signing.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_FAIL_CREATE': 4,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_TOPIC_SET_FAIL_SEND': 5
+}
+
+TOX_ERR_GROUP_SEND_MESSAGE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_GROUP_NOT_FOUND': 1,
+
+ #
+ # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_TOO_LONG': 2,
+
+ #
+ # The message pointer is null or length is zero.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_EMPTY': 3,
+
+ #
+ # The message type is invalid.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_BAD_TYPE': 4,
+
+ #
+ # The caller does not have the required permissions to send group messages.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_PERMISSIONS': 5,
+
+ #
+ # Packet failed to send.
+ #
+ 'TOX_ERR_GROUP_SEND_MESSAGE_FAIL_SEND': 6
+}
+
+TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ID passed did not designate a valid peer.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PEER_NOT_FOUND': 2,
+
+ #
+ # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_TOO_LONG': 3,
+
+ #
+ # The message pointer is null or length is zero.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_EMPTY': 4,
+
+ #
+ # The caller does not have the required permissions to send group messages.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_PERMISSIONS': 5,
+
+ #
+ # Packet failed to send.
+ #
+ 'TOX_ERR_GROUP_SEND_PRIVATE_MESSAGE_FAIL_SEND': 6
+}
+
+TOX_ERR_GROUP_SEND_CUSTOM_PACKET = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_GROUP_NOT_FOUND': 1,
+
+ #
+ # Message length exceeded 'TOX_MAX_MESSAGE_LENGTH.
+ #
+ 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_TOO_LONG': 2,
+
+ #
+ # The message pointer is null or length is zero.
+ #
+ 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_EMPTY': 3,
+
+ #
+ # The caller does not have the required permissions to send group messages.
+ #
+ 'TOX_ERR_GROUP_SEND_CUSTOM_PACKET_PERMISSIONS': 4
+}
+
+TOX_ERR_GROUP_INVITE_FRIEND = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_INVITE_FRIEND_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_INVITE_FRIEND_GROUP_NOT_FOUND': 1,
+
+ #
+ # The friend number passed did not designate a valid friend.
+ #
+ 'TOX_ERR_GROUP_INVITE_FRIEND_FRIEND_NOT_FOUND': 2,
+
+ #
+ # Creation of the invite packet failed. This indicates a network related error.
+ #
+ 'TOX_ERR_GROUP_INVITE_FRIEND_INVITE_FAIL': 3,
+
+ #
+ # Packet failed to send.
+ #
+ 'TOX_ERR_GROUP_INVITE_FRIEND_FAIL_SEND': 4
+}
+
+TOX_ERR_GROUP_INVITE_ACCEPT = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_INVITE_ACCEPT_OK': 0,
+
+ #
+ # The invite data is not in the expected format.
+ #
+ 'TOX_ERR_GROUP_INVITE_ACCEPT_BAD_INVITE': 1,
+
+ #
+ # The group instance failed to initialize.
+ #
+ 'TOX_ERR_GROUP_INVITE_ACCEPT_INIT_FAILED': 2,
+
+ #
+ # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE.
+ #
+ 'TOX_ERR_GROUP_INVITE_ACCEPT_TOO_LONG': 3
+}
+
+TOX_GROUP_JOIN_FAIL = {
+
+ #
+ # You are using the same nickname as someone who is already in the group.
+ #
+ 'TOX_GROUP_JOIN_FAIL_NAME_TAKEN': 0,
+
+ #
+ # The group peer limit has been reached.
+ #
+ 'TOX_GROUP_JOIN_FAIL_PEER_LIMIT': 1,
+
+ #
+ # You have supplied an invalid password.
+ #
+ 'TOX_GROUP_JOIN_FAIL_INVALID_PASSWORD': 2,
+
+ #
+ # The join attempt failed due to an unspecified error. This often occurs when the group is
+ # not found in the DHT.
+ #
+ 'TOX_GROUP_JOIN_FAIL_UNKNOWN': 3
+}
+
+TOX_ERR_GROUP_FOUNDER_SET_PASSWORD = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_GROUP_NOT_FOUND': 1,
+
+ #
+ # The caller does not have the required permissions to set the password.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_PERMISSIONS': 2,
+
+ #
+ # Password length exceeded 'TOX_GROUP_MAX_PASSWORD_SIZE.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_TOO_LONG': 3,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PASSWORD_FAIL_SEND': 4
+}
+
+TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_GROUP_NOT_FOUND': 1,
+
+ #
+ # 'TOX_GROUP_PRIVACY_STATE is an invalid type.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_INVALID': 2,
+
+ #
+ # The caller does not have the required permissions to set the privacy state.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_PERMISSIONS': 3,
+
+ #
+ # The privacy state could not be set. This may occur due to an error related to
+ # cryptographic signing of the new shared state.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SET': 4,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PRIVACY_STATE_FAIL_SEND': 5
+}
+
+TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_GROUP_NOT_FOUND': 1,
+
+ #
+ # The caller does not have the required permissions to set the peer limit.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_PERMISSIONS': 2,
+
+ #
+ # The peer limit could not be set. This may occur due to an error related to
+ # cryptographic signing of the new shared state.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SET': 3,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_FOUNDER_SET_PEER_LIMIT_FAIL_SEND': 4
+}
+
+TOX_ERR_GROUP_TOGGLE_IGNORE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_TOGGLE_IGNORE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_TOGGLE_IGNORE_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ID passed did not designate a valid peer.
+ #
+ 'TOX_ERR_GROUP_TOGGLE_IGNORE_PEER_NOT_FOUND': 2
+}
+
+TOX_ERR_GROUP_MOD_SET_ROLE = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ID passed did not designate a valid peer. Note: you cannot set your own role.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_PEER_NOT_FOUND': 2,
+
+ #
+ # The caller does not have the required permissions for this action.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_PERMISSIONS': 3,
+
+ #
+ # The role assignment is invalid. This will occur if you try to set a peer's role to
+ # the role they already have.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_ASSIGNMENT': 4,
+
+ #
+ # The role was not successfully set. This may occur if something goes wrong with role setting': ,
+ # or if the packet fails to send.
+ #
+ 'TOX_ERR_GROUP_MOD_SET_ROLE_FAIL_ACTION': 5
+}
+
+TOX_ERR_GROUP_MOD_REMOVE_PEER = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ID passed did not designate a valid peer.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PEER_NOT_FOUND': 2,
+
+ #
+ # The caller does not have the required permissions for this action.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_PERMISSIONS': 3,
+
+ #
+ # The peer could not be removed from the group.
+ #
+ # If a ban was set': , this error indicates that the ban entry could not be created.
+ # This is usually due to the peer's IP address already occurring in the ban list. It may also
+ # be due to the entry containing invalid peer information': , or a failure to cryptographically
+ # authenticate the entry.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_ACTION': 4,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_PEER_FAIL_SEND': 5
+}
+
+TOX_ERR_GROUP_MOD_REMOVE_BAN = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_BAN_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_BAN_GROUP_NOT_FOUND': 1,
+
+ #
+ # The caller does not have the required permissions for this action.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_BAN_PERMISSIONS': 2,
+
+ #
+ # The ban entry could not be removed. This may occur if ban_id does not designate
+ # a valid ban entry.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_ACTION': 3,
+
+ #
+ # The packet failed to send.
+ #
+ 'TOX_ERR_GROUP_MOD_REMOVE_BAN_FAIL_SEND': 4
+}
+
+TOX_GROUP_MOD_EVENT = {
+
+ #
+ # A peer has been kicked from the group.
+ #
+ 'KICK': 0,
+
+ #
+ # A peer has been banned from the group.
+ #
+ 'BAN': 1,
+
+ #
+ # A peer as been given the observer role.
+ #
+ 'OBSERVER': 2,
+
+ #
+ # A peer has been given the user role.
+ #
+ 'USER': 3,
+
+ #
+ # A peer has been given the moderator role.
+ #
+ 'MODERATOR': 4,
+}
+
+TOX_ERR_GROUP_BAN_QUERY = {
+
+ #
+ # The function returned successfully.
+ #
+ 'TOX_ERR_GROUP_BAN_QUERY_OK': 0,
+
+ #
+ # The group number passed did not designate a valid group.
+ #
+ 'TOX_ERR_GROUP_BAN_QUERY_GROUP_NOT_FOUND': 1,
+
+ #
+ # The ban_id does not designate a valid ban list entry.
+ #
+ 'TOX_ERR_GROUP_BAN_QUERY_BAD_ID': 2,
+}
+
+
+TOX_GROUP_BAN_TYPE = {
+
+ 'IP_PORT': 0,
+
+ 'PUBLIC_KEY': 1,
+
+ 'NICK': 2
+}
+
+TOX_PUBLIC_KEY_SIZE = 32
+
+TOX_ADDRESS_SIZE = TOX_PUBLIC_KEY_SIZE + 6
+
+TOX_MAX_FRIEND_REQUEST_LENGTH = 1016
+
+TOX_MAX_MESSAGE_LENGTH = 1372
+
+TOX_GROUP_MAX_TOPIC_LENGTH = 512
+
+TOX_GROUP_MAX_PART_LENGTH = 128
+
+TOX_GROUP_MAX_GROUP_NAME_LENGTH = 48
+
+TOX_GROUP_MAX_PASSWORD_SIZE = 32
+
+TOX_GROUP_CHAT_ID_SIZE = 32
+
+TOX_GROUP_PEER_PUBLIC_KEY_SIZE = 32
+
+TOX_MAX_NAME_LENGTH = 128
+
+TOX_MAX_STATUS_MESSAGE_LENGTH = 1007
+
+TOX_SECRET_KEY_SIZE = 32
+
+TOX_FILE_ID_LENGTH = 32
+
+TOX_HASH_LENGTH = 32
+
+TOX_MAX_CUSTOM_PACKET_SIZE = 1373
diff --git a/tox_wrapper/toxencryptsave.py b/tox_wrapper/toxencryptsave.py
new file mode 100644
index 0000000..631faa4
--- /dev/null
+++ b/tox_wrapper/toxencryptsave.py
@@ -0,0 +1,91 @@
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+try:
+ from tox_wrapper import libtox
+ import tox_wrapper.toxencryptsave_enums_and_consts as enum
+except:
+ import libtox
+ import toxencryptsave_enums_and_consts as enum
+
+from typing import Union, Callable
+from ctypes import (ArgumentError, byref, c_bool, c_char_p, c_int, c_size_t,
+ create_string_buffer, Array)
+def ToxError(ArgumentError): pass
+
+class ToxEncryptSave:
+
+ def __init__(self):
+ self.libtoxencryptsave = libtox.LibToxEncryptSave()
+
+ def is_data_encrypted(self, data: bytes) -> bool:
+ """
+ Checks if given data is encrypted
+ """
+ func = self.libtoxencryptsave.tox_is_data_encrypted
+ func.restype = c_bool
+ result = func(c_char_p(bytes(data)))
+ return bool(result)
+
+ def pass_encrypt(self, data: bytes, password: Union[str,bytes]) -> bytes:
+ """
+ Encrypts the given data with the given password.
+
+ :return: output array
+ """
+ out = create_string_buffer(len(data) + enum.TOX_PASS_ENCRYPTION_EXTRA_LENGTH)
+ tox_err_encryption = c_int()
+ assert password
+ if type(password) != bytes:
+ password = bytes(password, 'utf-8')
+ self.libtoxencryptsave.tox_pass_encrypt(c_char_p(data),
+ c_size_t(len(data)),
+ c_char_p(password),
+ c_size_t(len(password)),
+ out,
+ byref(tox_err_encryption))
+ tox_err_encryption = tox_err_encryption.value
+ if tox_err_encryption == enum.TOX_ERR_ENCRYPTION['OK']:
+ return bytes(out[:])
+ if tox_err_encryption == enum.TOX_ERR_ENCRYPTION['NULL']:
+ raise ArgumentError('Some input data, or maybe the output pointer, was null.')
+ if tox_err_encryption == enum.TOX_ERR_ENCRYPTION['KEY_DERIVATION_FAILED']:
+ raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a'
+ ' lack of memory issue. The functions accepting keys do not produce this error.')
+ if tox_err_encryption == enum.TOX_ERR_ENCRYPTION['FAILED']:
+ raise RuntimeError('The encryption itself failed.')
+ raise ToxError('The function did not return OK.')
+
+ def pass_decrypt(self, data: bytes, password: Union[str,bytes]) -> bytes:
+ """
+ Decrypts the given data with the given password.
+
+ :return: output array
+ """
+ out = create_string_buffer(len(data) - enum.TOX_PASS_ENCRYPTION_EXTRA_LENGTH)
+ tox_err_decryption = c_int()
+ assert password
+ if type(password) != bytes:
+ password = bytes(password, 'utf-8')
+ self.libtoxencryptsave.tox_pass_decrypt(c_char_p(bytes(data)),
+ c_size_t(len(data)),
+ c_char_p(password),
+ c_size_t(len(password)),
+ out,
+ byref(tox_err_decryption))
+ tox_err_decryption = tox_err_decryption.value
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['OK']:
+ return bytes(out[:])
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['NULL']:
+ raise ArgumentError('Some input data, or maybe the output pointer, was null.')
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['INVALID_LENGTH']:
+ raise ArgumentError('The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes')
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['BAD_FORMAT']:
+ raise ArgumentError('The input data is missing the magic number (i.e. wasn\'t created by this module, or is'
+ ' corrupted)')
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['KEY_DERIVATION_FAILED']:
+ raise RuntimeError('The crypto lib was unable to derive a key from the given passphrase, which is usually a'
+ ' lack of memory issue. The functions accepting keys do not produce this error.')
+ if tox_err_decryption == enum.TOX_ERR_DECRYPTION['FAILED']:
+ raise RuntimeError('The encrypted byte array could not be decrypted. Either the data was corrupt or the '
+ 'password/key was incorrect.')
+ raise ToxError('The function did not return OK.')
diff --git a/tox_wrapper/toxencryptsave_enums_and_consts.py b/tox_wrapper/toxencryptsave_enums_and_consts.py
new file mode 100644
index 0000000..cf795f8
--- /dev/null
+++ b/tox_wrapper/toxencryptsave_enums_and_consts.py
@@ -0,0 +1,29 @@
+TOX_ERR_ENCRYPTION = {
+ # The function returned successfully.
+ 'OK': 0,
+ # Some input data, or maybe the output pointer, was null.
+ 'NULL': 1,
+ # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
+ # functions accepting keys do not produce this error.
+ 'KEY_DERIVATION_FAILED': 2,
+ # The encryption itself failed.
+ 'FAILED': 3
+}
+
+TOX_ERR_DECRYPTION = {
+ # The function returned successfully.
+ 'OK': 0,
+ # Some input data, or maybe the output pointer, was null.
+ 'NULL': 1,
+ # The input data was shorter than TOX_PASS_ENCRYPTION_EXTRA_LENGTH bytes
+ 'INVALID_LENGTH': 2,
+ # The input data is missing the magic number (i.e. wasn't created by this module, or is corrupted)
+ 'BAD_FORMAT': 3,
+ # The crypto lib was unable to derive a key from the given passphrase, which is usually a lack of memory issue. The
+ # functions accepting keys do not produce this error.
+ 'KEY_DERIVATION_FAILED': 4,
+ # The encrypted byte array could not be decrypted. Either the data was corrupt or the password/key was incorrect.
+ 'FAILED': 5,
+}
+
+TOX_PASS_ENCRYPTION_EXTRA_LENGTH = 80
diff --git a/tox_wrapper/toxygen_echo.py b/tox_wrapper/toxygen_echo.py
new file mode 100644
index 0000000..4f43939
--- /dev/null
+++ b/tox_wrapper/toxygen_echo.py
@@ -0,0 +1,450 @@
+#!/var/local/bin/python3.bash
+# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
+
+# A work in progress - chat works.
+""" echo.py features
+ - accept friend request
+ - echo back friend message
+# - accept and answer friend call request
+# - send back friend audio/video data
+# - send back files friend sent
+"""
+
+import sys
+import os
+import traceback
+import threading
+import random
+from ctypes import *
+import time
+
+# LOG=util.log
+global LOG
+import logging
+# log = lambda x: LOG.info(x)
+LOG = logging.getLogger('app')
+def LOG_error(a): print('EROR_ '+a)
+def LOG_warn(a): print('WARN_ '+a)
+def LOG_info(a): print('INFO_ '+a)
+def LOG_debug(a): print('DBUG_ '+a)
+def LOG_trace(a): pass # print('TRAC_ '+a)
+
+from tox_wrapper import tox
+import tox_wrapper.toxcore_enums_and_consts as enums
+from tox_wrapper.tox import Tox, UINT32_MAX
+from tox_wrapper.toxcore_enums_and_consts import TOX_CONNECTION, TOX_USER_STATUS, \
+ TOX_MESSAGE_TYPE, TOX_PUBLIC_KEY_SIZE, TOX_FILE_CONTROL, TOX_FILE_KIND
+
+import tox_wrapper.tests.support_testing as ts
+from tox_wrapper.tests.support_testing import oMainArgparser
+
+def sleep(fSec):
+ if 'QtCore' in globals():
+ if fSec > .000001: QtCore.QThread.msleep(fSec)
+ QtCore.QCoreApplication.processEvents()
+ else:
+ time.sleep(fSec)
+
+try:
+ import coloredlogs
+ if 'COLOREDLOGS_LEVEL_STYLES' not in os.environ:
+ os.environ['COLOREDLOGS_LEVEL_STYLES'] = 'spam=22;debug=28;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red'
+except ImportError as e:
+ # logging.log(logging.DEBUG, f"coloredlogs not available: {e}")
+ coloredlogs = None
+
+if 'USER' in os.environ:
+ sDATA_FILE = '/tmp/logging_toxygen_' +os.environ['USER'] +'.tox'
+elif 'USERNAME' in os.environ:
+ sDATA_FILE = '/tmp/logging_toxygen_' +os.environ['USERNAME'] +'.tox'
+else:
+ sDATA_FILE = '/tmp/logging_toxygen_' +'data' +'.tox'
+
+bHAVE_AV = False
+iDHT_TRIES = 100
+iDHT_TRY = 0
+
+#?SERVER = lLOCAL[-1]
+
+if not bHAVE_AV:
+ class AV(): pass
+else:
+ class AV(tox.ToxAV):
+ def __init__(self, core):
+ super(AV, self).__init__(core)
+ self.core = self.get_tox()
+
+ def on_call(self, fid, audio_enabled, video_enabled) -> None:
+ LOG.info("Incoming %s call from %d:%s ..." % (
+ "video" if video_enabled else "audio", fid,
+ self.core.friend_get_name(fid)))
+ bret = self.answer(fid, 48, 64)
+ LOG.info(f"Answered, in call... {bret}")
+
+ def on_call_state(self, fid, state) -> None:
+ LOG.info('call state:fn=%d, state=%d' % (fid, state))
+
+ def on_audio_bit_rate(self, fid, audio_bit_rate) -> None:
+ LOG.info('audio bit rate status: fn=%d, abr=%d' %
+ (fid, audio_bit_rate))
+
+ def on_video_bit_rate(self, fid, video_bit_rate) -> None:
+ LOG.info('video bit rate status: fn=%d, vbr=%d' %
+ (fid, video_bit_rate))
+
+ def on_audio_receive_frame(self, fid, pcm, sample_count,
+ channels, sampling_rate) -> None:
+ # LOG.info('audio frame: %d, %d, %d, %d' %
+ # (fid, sample_count, channels, sampling_rate))
+ # LOG.info('pcm len:%d, %s' % (len(pcm), str(type(pcm))))
+ sys.stdout.write('.')
+ sys.stdout.flush()
+ bret = self.audio_send_frame(fid, pcm, sample_count,
+ channels, sampling_rate)
+ if bret is False:
+ LOG.error('on_audio_receive_frame error.')
+
+ def on_video_receive_frame(self, fid, width, height, frame, u, v) -> None:
+ LOG.info('video frame: %d, %d, %d, ' % (fid, width, height))
+ sys.stdout.write('*')
+ sys.stdout.flush()
+ bret = self.video_send_frame(fid, width, height, frame, u, v)
+ if bret is False:
+ LOG.error('on_video_receive_frame error.')
+
+ def witerate(self) -> None:
+ self.iterate()
+
+
+def save_to_file(tox, fname):
+ data = tox.get_savedata()
+ with open(fname, 'wb') as f:
+ f.write(data)
+
+def load_from_file(fname):
+ assert os.path.exists(fname)
+ return open(fname, 'rb').read()
+
+class EchoBot():
+ def __init__(self, oTox):
+ self._tox = oTox
+ self._tox.self_set_name("PyEchoBot")
+ LOG.info(f'ID: {self._tox.self_get_address()}')
+
+ self.files = {}
+ self.av = None
+ self.on_connection_status = None
+
+ def start(self) -> None:
+ self.connect()
+ if bHAVE_AV:
+ # RuntimeError: Attempted to create a second session for the same Tox instance.
+
+ self.av = True # AV(self._tox_pointer)
+ def bobs_on_friend_request(iTox,
+ public_key,
+ message_data,
+ message_data_size,
+ *largs) -> None:
+ key = ''.join(chr(x) for x in public_key[:TOX_PUBLIC_KEY_SIZE])
+ sPk = tox.bin_to_string(key, TOX_PUBLIC_KEY_SIZE)
+ sMd = str(message_data, 'UTF-8')
+ LOG.debug('on_friend_request ' +sPk +' ' +sMd)
+ self.on_friend_request(sPk, sMd)
+ LOG.info('setting bobs_on_friend_request')
+ self._tox.callback_friend_request(bobs_on_friend_request)
+
+ def bobs_on_friend_message(iTox,
+ iFriendNum,
+ iMessageType,
+ message_data,
+ message_data_size,
+ *largs) -> None:
+ sMd = str(message_data, 'UTF-8')
+ LOG_debug(f"on_friend_message {iFriendNum}" +' ' +sMd)
+ self.on_friend_message(iFriendNum, iMessageType, sMd)
+ LOG.info('setting bobs_on_friend_message')
+ self._tox.callback_friend_message(bobs_on_friend_message)
+
+ def bobs_on_file_chunk_request(iTox, fid, filenumber, position, length, *largs) -> None:
+ if length == 0:
+ return
+
+ data = self.files[(fid, filenumber)]['f'][position:(position + length)]
+ self._tox.file_send_chunk(fid, filenumber, position, data)
+ self._tox.callback_file_chunk_request(bobs_on_file_chunk_request)
+
+ def bobs_on_file_recv(iTox, fid, filenumber, kind, size, filename, *largs):
+ LOG_info(f"on_file_recv {fid} {filenumber} {kind} {size} {filename}")
+ if size == 0:
+ return
+ self.files[(fid, filenumber)] = {
+ 'f': bytes(),
+ 'filename': filename,
+ 'size': size
+ }
+ self._tox.file_control(fid, filenumber, TOX_FILE_CONTROL['RESUME'])
+
+
+ def connect(self) -> None:
+ if not self.on_connection_status:
+ def on_connection_status(iTox, iCon, *largs):
+ LOG_info('ON_CONNECTION_STATUS - CONNECTED ' + repr(iCon))
+ self._tox.callback_self_connection_status(on_connection_status)
+ LOG.info('setting on_connection_status callback ')
+ self.on_connection_status = on_connection_status
+ if self._oargs.network in ['newlocal', 'local']:
+ LOG.info('connecting on the new network ')
+ sNet = 'newlocal'
+ elif self._oargs.network == 'new':
+ LOG.info('connecting on the new network ')
+ sNet = 'new'
+ else: # main old
+ LOG.info('connecting on the old network ')
+ sNet = 'old'
+ sFile = self._oargs.nodes_json
+ lNodes = ts.generate_nodes_from_file(sFile)
+ lElts = lNodes
+ random.shuffle(lElts)
+ for lElt in lElts[:10]:
+ status = self._tox.self_get_connection_status()
+ try:
+ if self._tox.bootstrap(*lElt):
+ LOG.info('connected to ' + lElt[0]+' '+repr(status))
+ else:
+ LOG.warn('failed connecting to ' + lElt[0])
+ except Exception as e:
+ LOG.warn('error connecting to ' + lElt[0])
+
+ if self._oargs.proxy_type > 0:
+ random.shuffle(lElts)
+ for lElt in lElts[:10]:
+ status = self._tox.self_get_connection_status()
+ try:
+ if self._tox.add_tcp_relay(*lElt):
+ LOG.info('relayed to ' + lElt[0] +' '+repr(status))
+ else:
+ LOG.warn('failed relay to ' + lElt[0])
+ except Exception as e:
+ LOG.warn('error relay to ' + lElt[0])
+
+ def loop(self) -> None:
+ if not self.av:
+ self.start()
+ checked = False
+ save_to_file(self._tox, sDATA_FILE)
+
+ LOG.info('Starting loop.')
+ while True:
+
+ status = self._tox.self_get_connection_status()
+ if not checked and status:
+ LOG.info('Connected to DHT.')
+ checked = True
+ if not checked and not status:
+ global iDHT_TRY
+ iDHT_TRY += 10
+ self.connect()
+ self.iterate(100)
+ if iDHT_TRY >= iDHT_TRIES:
+ raise RuntimeError("Failed to connect to the DHT.")
+ LOG.warn(f"NOT Connected to DHT. {iDHT_TRY}")
+ checked = True
+ if checked and not status:
+ LOG.info('Disconnected from DHT.')
+ self.connect()
+ checked = False
+
+ if bHAVE_AV:
+ True # self.av.witerate()
+ self.iterate(100)
+
+ LOG.info('Ending loop.')
+
+ def iterate(self, n=100) -> None:
+ interval = self._tox.iteration_interval()
+ for i in range(n):
+ self._tox.iterate()
+ sleep(interval / 1000.0)
+ self._tox.iterate()
+
+ def on_friend_request(self, pk, message) -> None:
+ LOG.debug('Friend request from %s: %s' % (pk, message))
+ self._tox.friend_add_norequest(pk)
+ LOG.info('on_friend_request Accepted.')
+ save_to_file(self._tox, sDATA_FILE)
+
+ def on_friend_message(self, friendId, message_type , message) -> None:
+ name = self._tox.friend_get_name(friendId)
+ LOG.debug(f"{name}, {message}, {message_type}")
+ yMessage = bytes(message, 'UTF-8')
+ self._tox.friend_send_message(friendId, TOX_MESSAGE_TYPE['NORMAL'], yMessage)
+ LOG.info('EchoBot sent: %s' % message)
+
+ def on_file_recv_chunk(self, fid, filenumber, position, data) -> None:
+ filename = self.files[(fid, filenumber)]['filename']
+ size = self.files[(fid, filenumber)]['size']
+ LOG.debug(f"on_file_recv_chunk {fid} {filenumber} {filename} {position/float(size)*100}")
+
+ if data is None:
+ msg = "I got '{}', sending it back right away!".format(filename)
+ self._tox.friend_send_message(fid, TOX_MESSAGE_TYPE['NORMAL'], msg)
+
+ self.files[(fid, 0)] = self.files[(fid, filenumber)]
+
+ length = self.files[(fid, filenumber)]['size']
+ self._tox.file_send(fid, TOX_FILE_KIND['DATA'], length, filename)
+
+ del self.files[(fid, filenumber)]
+ return
+
+ self.files[(fid, filenumber)]['f'] += data
+
+class App():
+ def __init__(self):
+ self.mode = 0
+oAPP = App()
+
+class EchobotTox(Tox):
+
+ def __init__(self, opts, app=None):
+
+ super().__init__(opts, app=app)
+ self._address = self.self_get_address()
+ self.name = 'pyechobot'
+ self._opts = opts
+ self._app = app
+
+class BaseThread(threading.Thread):
+
+ def __init__(self, name=None, target=None):
+ if name:
+ super().__init__(name=name, target=target)
+ else:
+ super().__init__(target=target)
+ self._stop_thread = False
+ self.name = name
+
+ def stop_thread(self, timeout=-1) -> None:
+ self._stop_thread = True
+ if timeout < 0:
+ timeout = ts.iTHREAD_TIMEOUT
+ i = 0
+ while i < ts.iTHREAD_JOINS:
+ self.join(timeout)
+ if not self.is_alive(): break
+ i = i + 1
+ else:
+ LOG.warning(f"{self.name} BLOCKED")
+
+class ToxIterateThread(BaseThread):
+
+ def __init__(self, tox):
+ super().__init__(name='ToxIterateThread')
+ self._tox = tox
+
+ def run(self) -> None:
+ while not self._stop_thread:
+ self._tox.iterate()
+ sleep(self._tox.iteration_interval() / 1000)
+
+def oArgparse(lArgv):
+ parser = ts.oMainArgparser()
+ parser.add_argument('--norequest',type=str, default='False',
+ choices=['True','False'],
+ help='Use _norequest')
+ parser.add_argument('profile', type=str, nargs='?', default=None,
+ help='Path to Tox profile')
+ oArgs = parser.parse_args(lArgv)
+
+ for key in ts.lBOOLEANS:
+ if key not in oArgs: continue
+ val = getattr(oArgs, key)
+ setattr(oArgs, key, bool(val))
+
+ if hasattr(oArgs, 'sleep'):
+ if oArgs.sleep == 'qt':
+ pass # broken or gevent.sleep(idle_period)
+ elif oArgs.sleep == 'gevent':
+ pass # broken or gevent.sleep(idle_period)
+ else:
+ oArgs.sleep = 'time'
+
+ return oArgs
+
+def iMain(oArgs) -> int:
+ global sDATA_FILE
+ # oTOX_OPTIONS = ToxOptions()
+ global oTOX_OPTIONS
+ oMainArgparser
+ oTOX_OPTIONS = ts.oToxygenToxOptions(oArgs)
+ opts = oTOX_OPTIONS
+ if coloredlogs:
+ coloredlogs.install(
+ level=oArgs.loglevel,
+ logger=LOG,
+ # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d]
+ fmt='%(name)s %(levelname)s %(message)s'
+ )
+ else:
+ if 'logfile' in oArgs:
+ logging.basicConfig(filename=oArgs.logfile,
+ level=oArgs.loglevel,
+ format='%(levelname)-8s %(message)s')
+ else:
+ logging.basicConfig(level=oArgs.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ iRet = 0
+ if hasattr(oArgs,'profile') and oArgs.profile and os.path.isfile(oArgs.profile):
+ sDATA_FILE = oArgs.profile
+ LOG.info(f"loading from {sDATA_FILE}")
+ opts.savedata_data = load_from_file(sDATA_FILE)
+ opts.savedata_length = len(opts.savedata_data)
+ opts.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE']
+ else:
+ opts.savedata_data = None
+
+ try:
+ oTox = EchobotTox(opts, app=oAPP)
+ t = EchoBot(oTox)
+ t._oargs = oArgs
+ t.start()
+ t.loop()
+ save_to_file(t._tox, sDATA_FILE)
+ except KeyboardInterrupt:
+ save_to_file(t._tox, sDATA_FILE)
+ except RuntimeError as e:
+ LOG.error(f"ERROR {e}")
+ iRet = 1
+ except Exception as e:
+ LOG.error(f"EXCEPTION {e}")
+ LOG.warn(' iMain(): ' \
+ +'\n' + traceback.format_exc())
+ iRet = 1
+ return iRet
+
+def main(lArgs=None) -> int:
+ global oTOX_OARGS
+ global bIS_LOCAL
+ if lArgs is None: lArgs = []
+ oArgs = oArgparse(lArgs)
+ bIS_LOCAL = oArgs.network in ['newlocal', 'localnew', 'local']
+ oTOX_OARGS = oArgs
+ setattr(oTOX_OARGS, 'bIS_LOCAL', bIS_LOCAL)
+ global oTOX_OPTIONS
+ oTOX_OPTIONS = ts.oToxygenToxOptions(oArgs)
+ if coloredlogs:
+ # https://pypi.org/project/coloredlogs/
+ coloredlogs.install(level=oArgs.loglevel,
+ logger=LOG,
+ # %(asctime)s,%(msecs)03d %(hostname)s [%(process)d]
+ fmt='%(name)s %(levelname)s %(message)s'
+ )
+ else:
+ logging.basicConfig(level=oArgs.loglevel) # logging.INFO
+
+ return iMain(oArgs)
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))