First refactor

This commit is contained in:
emdee 2022-10-23 22:11:12 +00:00
parent 58a89710bd
commit 3882fe2ec9
2 changed files with 481 additions and 92 deletions

View file

@ -1,3 +1,27 @@
#Tox-Sync
A bot that sync messages between Freenode IRC #tox-ontopic and Tox group chat.
A bot that sync messages between IRC and Tox group chat.
## Hard forked
Hard forked to use https://git.macaw.me/emdee/toxygen_wrapper
Just clone that repo and put the resulting directory on your
```PYTHONPATH```.
Run: ```tox-irc-sync.py --help`` for command line arguments.
For the ```#tox``` group on ```libera.chat```:
For example```irc.libera.net#tox```:
```
python3 tox-irc-sync.py \
--nodes_json $HOME/.config/tox/DHTnodes.json \
--irc_chan "#tor" --irc_host irc.libera.net --irc_port 6667 \
```
Libera will not work over Tor, but ```irc.oftc.net#tor``` will:
```
python3 tox-irc-sync.py \
--nodes_json $HOME/.config/tox/DHTnodes.json \
--irc_chan "#tor" --irc_host irc.oftc.net --irc_port 6667 \
--proxy_type 2 --proxy_host 127.0.0.1 --proxy_port 9050
```

View file

@ -1,43 +1,56 @@
import sys
import os
import socket
import string
import select
import re
import pickle
from pytox import Tox, ToxAV
import logging
import readline
import ctypes
from time import sleep
from os.path import exists
from threading import Thread
from random import shuffle
import wrapper
from wrapper.tox import Tox
from wrapper.toxav import ToxAV
import wrapper.toxcore_enums_and_consts as enums
from wrapper.toxcore_enums_and_consts import \
TOX_CONNECTION, TOX_USER_STATUS, TOX_MESSAGE_TYPE, \
TOX_SECRET_KEY_SIZE, TOX_FILE_CONTROL, TOX_ADDRESS_SIZE, \
TOX_GROUP_PRIVACY_STATE, TOX_GROUP_ROLE
try:
import support_testing as ts
except ImportError:
import wrapper_tests.support_testing as ts
global LOG
LOG = logging.getLogger('app.'+'ts')
SERVER = ['54.199.139.199', 33445, '7F9C31FE850E97CEFD4C4591DF93FC757C7C12549DDD55F8EEAECC34FE76C029']
GROUP_BOT = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5'
PWD = ''
IRC_HOST = 'irc.freenode.net'
IRC_PORT = 6667
NAME = NICK = IDENT = REALNAME = 'SyncBot'
CHANNEL = '#tox-ontopic'
MEMORY_DB = 'memory.pickle'
NAME = NICK = IDENT = REALNAME = 'SyniTox'
class AV(ToxAV):
def __init__(self, core, max_calls):
self.core = self.get_tox()
def __init__(self, core):
self.core = core
self.cs = None
self.call_type = self.TypeAudio
self.call_type = None
def on_invite(self, idx):
self.cs = self.get_peer_csettings(idx, 0)
self.call_type = self.cs['call_type']
print('Incoming %s call from %d:%s ...' % (
LOG.info('Incoming %s call from %d:%s ...' % (
'video' if self.call_type == self.TypeVideo else 'audio', idx,
self.core.get_name(self.get_peer_id(idx, 0))))
self.answer(idx, self.call_type)
print('Answered, in call...')
LOG.info('Answered, in call...')
def on_start(self, idx):
self.change_settings(idx, {'max_video_width': 1920,
@ -48,7 +61,7 @@ class AV(ToxAV):
def on_end(self, idx):
self.kill_transmission()
print('Call ended')
LOG.info('Call ended')
def on_peer_timeout(self, idx):
self.stop_call()
@ -63,40 +76,195 @@ class AV(ToxAV):
sys.stdout.flush()
self.send_video(idx, width, height, data)
bot_toxname = 'SyncBot'
bot_toxname = 'SyniTox'
class SyncBot(Tox):
def __init__(self):
if exists('data'):
self.load_from_file('data')
class SyniTox(Tox):
def __init__(self, opts,
sChannel='#tor',
sIRC_HOST='irc.oftc.net',
iIRC_PORT=6667,
GROUP_BOT_PK = '',
sMEMORY_DB = ''
):
Tox.__init__(self, tox_options=opts)
self._address = self.self_get_address()
self._opts = opts
self._app = None
self._settings = {}
self._sChannel = sChannel
self.sIRC_HOST = sIRC_HOST
self.iIRC_PORT = iIRC_PORT
self.sGROUP_BOT_PK = GROUP_BOT_PK
self.sMEMORY_DB = sMEMORY_DB
global oTOX_OARGS
self._oArgs = oTOX_OARGS
data = self._oArgs.profile
if data and os.path.exists(data):
self.load_from_file(data)
self.av = AV(self, 10)
self.connect()
self.set_name(bot_toxname)
self.set_status_message("Send me a message with the word 'invite'")
print('ID: %s' % self.get_address())
self.av = self.AV
self.irc = None
self.bid = -1
self._bRouted = None
def start(self):
self.self_set_name(bot_toxname)
self.self_set_status_message("Send me a message with the word 'invite'")
LOG.info('Our ToxID: %s' % self.self_get_toxid())
self.readbuffer = ''
self.readbuffer = b''
self.tox_group_id = None
self.irc_init()
self.group_init()
self.memory = {}
if exists(MEMORY_DB):
with open(MEMORY_DB, 'r') as f:
if os.path.exists(self.sMEMORY_DB):
with open(self.sMEMORY_DB, 'r') as f:
self.memory = pickle.load(f)
self.irc_init()
b = self.test_net()
if b:
self.dht_init()
def bRouted(self):
if self._oArgs.network not in ['local', 'localnew', 'newlocal']:
b = ts.bAreWeConnected()
if b is None:
i = os.system('ip route|grep ^def')
if i > 0:
b = False
else:
b = True
if not b:
LOG.warn("No default route for network " +self._oArgs.network)
return False
return b
return True
def test_net(self, lElts=None, oThread=None, iMax=4):
# bootstrap
lNodes = ts.generate_nodes(oArgs=self._oArgs,
ipv='ipv4',
udp_not_tcp=True)
self._settings['current_nodes_udp'] = lNodes.copy()
if not lNodes:
LOG.warn('empty generate_nodes udp')
else:
LOG.debug(f'Called generate_nodes: udp {len(lNodes)}')
lNodes = ts.generate_nodes(oArgs=self._oArgs,
ipv='ipv4',
udp_not_tcp=False)
self._settings['current_nodes_tcp'] = lNodes
if not lNodes:
LOG.warn('empty generate_nodes tcp')
else:
LOG.debug(f'Called generate_nodes: tcp {len(lNodes)}')
# if oThread and oThread._stop_thread: return
LOG.debug("test_net network=" +self._oArgs.network +' iMax=' +str(iMax))
return True
def group_init(self):
LOG.debug(f"group_init proxy={self._oArgs.proxy_type}")
group_name = bot_toxname +' Test ' +self._sChannel
if not self.sGROUP_BOT_PK:
privacy_state = TOX_GROUP_PRIVACY_STATE['PUBLIC']
nick = bot_toxname +self._sChannel
status = TOX_USER_STATUS['NONE']
num = self.group_new(privacy_state, group_name, nick, status)
assert num >= 0, num
pk = self.group_self_get_public_key(num)
assert pk, pk
self.sGROUP_BOT_PK = pk
self.sGROUP_NUM = num
self.group_set_topic(num, bot_toxname +" IRC")
LOG.info(f"group_init GROUP_BOT_PK={self.sGROUP_BOT_PK}")
#? self.tox_group_id = self.bid
self.group_send_message(num, TOX_MESSAGE_TYPE['NORMAL'], "hi")
# TOX_GROUP_ROLE['FOUNDER']
self.init_callbacks()
def init_callbacks(self):
def gi_wrapped(iTox, friendid, invite_data, invite_len, *args):
invite_data = str(invite_data, 'UTF-8')
self.on_group_invite(friendid, invite_data)
self.callback_group_invite(gi_wrapped, 0)
def scs_wrapped(iTox, friendid, *args):
self.on_connection_status(self, scs_wrapped)
self.callback_self_connection_status(scs_wrapped)
def gm_wrapped(iTox, groupnumber, peer_id, type_, message, mlen, *args):
message = str(message, 'UTF-8')
self.on_group_message(groupnumber, peer_id, message)
self.callback_group_message(gm_wrapped, 0)
def ga_wrapped(iTox, groupnumber, peer_id, type_, action, mlen, *args):
self.on_group_action(groupnumber, peer_id, action)
#? self.callback_group_action(ga_wrapped, 0)
def fr_wrapped(iTox, pk, message, mlen, *args):
message = str(message, 'UTF-8')
self.on_friend_request(self, pk, message)
self.callback_friend_request(fr_wrapped)
def fm_wrapped(iTox, peer_id, message, mlen, *args):
message = str(message, 'UTF-8')
self.on_friend_request(self, peer_id, message)
self.callback_friend_request(fm_wrapped)
def del_callbacks(self):
self.callback_group_invite(None, 0)
self.callback_self_connection_status(None)
self.callback_group_message(None, 0)
# self.callback_group_action(None, 0)
self.callback_friend_request(None)
self.callback_friend_request(None)
def irc_init(self):
self.irc = socket.socket()
self.irc.connect((IRC_HOST, IRC_PORT))
self.irc.send('NICK %s\r\n' % NICK)
self.irc.send('USER %s %s bla :%s\r\n' % (IDENT, IRC_HOST, REALNAME))
if not self.bRouted(): return
LOG.info(f"irc_init proxy={self._oArgs.proxy_type}")
if self._oArgs.proxy_type == 2:
from wrapper_tests import socks
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5,
self._oArgs.proxy_host,
self._oArgs.proxy_port)
irc = socks.socksocket()
else:
irc = socket.socket()
try:
irc.connect((self.sIRC_HOST, self.iIRC_PORT))
irc.send(bytes('NICK ' + NICK + '\r\n', 'UTF-8' ))
irc.send(bytes('USER %s %s bla :%s\r\n' % (IDENT, self.sIRC_HOST, REALNAME),
'UTF-8'))
except Exception as e:
LOG.warn(f'IRC error {e}')
else:
LOG.info('IRC connected ' +'NICK =' + NICK)
self.irc = irc
def connect(self):
print('connecting...')
self.bootstrap_from_address(SERVER[0], SERVER[1], SERVER[2])
def dht_init(self):
if not self.bRouted(): return
if 'current_nodes_udp' not in self._settings:
self.test_net()
lNodes = self._settings['current_nodes_udp']
shuffle(lNodes)
if self._oArgs.proxy_type == 0:
ts.bootstrap_good(lNodes[:4], [self])
else:
if self._bRouted == None:
LOG.info(f'DHT bootstapping 1')
ts.bootstrap_good([lNodes[0]], [self])
if 'current_nodes_tcp' not in self._settings:
self.test_net()
lNodes = self._settings['current_nodes_tcp']
shuffle(lNodes)
ts.bootstrap_tcp(lNodes[:4], [self])
def ensure_exe(self, func, args):
def ensure_exe(self, func, *args):
count = 0
THRESHOLD = 50
@ -106,42 +274,115 @@ class SyncBot(Tox):
except:
assert count < THRESHOLD
count += 1
for i in range(10):
self.do()
sleep(0.02)
self.do()
def loop(self):
def do(self, n=50):
interval = self.iteration_interval()
for i in range(n):
self.iterate()
sleep(interval / 1000.0 *10)
def unroute(self):
if self.irc:
try: irc.close()
except: pass
self.irc = None
def irc_check(self, lines):
if b'NOTICE AUTH' in lines[0]:
for line in lines[:99]:
if b'NOTICE AUTH' not in line: return
line = str(line, 'UTF-8').strip()
print(line)
else:
for line in lines[:5]:
line = str(line, 'UTF-8').strip().lower()
if 'banned' in line:
raise RuntimeError(line)
if 'error' in line and 'closing' in line:
raise RuntimeError(line)
def iLoop(self):
checked = False
self.joined = False
self.request = False
count = 0
try:
count = count + 1
while True:
status = self.isconnected()
if not checked and status:
print('Connected to DHT.')
b = self.bRouted()
if not b:
self.unroute()
checked = False
if self._bRouted is None or self._bRouted != b:
self._bRouted = b
if count % 6 == 1:
LOG.info(f'Not routed {count}')
sleep(10)
continue
else:
if self._bRouted is None:
self._bRouted = True
self.irc_send('.')
if self._bRouted is None or self._bRouted != b:
self._bRouted = b
LOG.debug(f'Routed {count}')
status = self.self_get_connection_status()
if not status:
if count % 6 == 1:
LOG.info(f'Not connected {count}')
self.dht_init()
if b and not checked and status:
LOG.info('Connected to DHT.')
checked = True
try:
self.bid = self.get_friend_id(GROUP_BOT)
except:
self.ensure_exe(self.add_friend, (GROUP_BOT, 'Hi'))
self.bid = self.get_friend_id(GROUP_BOT)
self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK)
LOG.info(f'Connected to group {self.bid}')
except ctypes.ArgumentError as e:
self.bid = None
if self.bid == None:
self.ensure_exe(self.friend_add_norequest, self.sGROUP_BOT_PK)
LOG.info(f'friend_add_n to group {self.sGROUP_BOT_PK[:8]}')
self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK)
LOG.info(f'Added to group {self.bid}')
num = self.sGROUP_NUM
my_pk = self.group_self_get_public_key(num)
LOG.info(f'Connected to group as {my_pk[:8]}')
if checked and not status:
print('Disconnected from DHT.')
self.connect()
if b and checked and not status:
LOG.info('Disconnected from DHT.')
self.dht_init()
checked = False
if not self.irc:
LOG.info('Disconnected from IRC.')
self.irc_init()
if not self.irc:
sleep(10)
continue
LOG.info('Waiting on IRC.')
readable, _, _ = select.select([self.irc], [], [], 0.1)
readable, _, _ = select.select([self.irc], [], [], 0.01)
if readable:
if not readable:
LOG.info('Waited on IRC but nothing to read.')
else:
self.readbuffer += self.irc.recv(4096)
lines = self.readbuffer.split('\n')
lines = self.readbuffer.split(b'\n')
self.irc_check(lines)
LOG.info(f'Waited on IRC and got {len(lines)} lines.')
self.readbuffer = lines.pop()
for line in lines:
line = str(line, 'UTF-8')
i = line.find(' ')
print(line[i+1:])
l = line.rstrip().split()
rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' %
CHANNEL, line, re.S)
self._sChannel, line, re.S)
if rx:
print('IRC> %s: %s' % rx.groups())
msg = '[%s]: %s' % rx.groups()
@ -151,31 +392,55 @@ class SyncBot(Tox):
action = '[%s]: %s' % (rx.group(1),
rx.group(2)[8:-1])
self.ensure_exe(self.group_action_send,
(self.tox_group_id, action))
self.tox_group_id, action)
elif self.tox_group_id != None:
self.ensure_exe(self.group_message_send,
(self.tox_group_id, msg))
self.tox_group_id, msg)
if content.startswith('^'):
self.handle_command(content)
l = line.rstrip().split()
if l[0] == 'PING':
elif l[0] == 'PING':
self.irc_send('PONG %s\r\n' % l[1])
if l[1] == '376':
self.irc.send('PRIVMSG NickServ :IDENTIFY %s %s\r\n'
% (NICK, PWD))
self.irc.send('JOIN %s\r\n' % CHANNEL)
elif l[1] == '376':
# :End of /MOTD command
self.irc.send(bytes('PRIVMSG NickServ :IDENTIFY %s %s\r\n'
% (NICK, PWD,), 'UTF-8'))
self.irc.send(bytes('JOIN %s\r\n' % self._sChannel, 'UTF-8'))
elif l[1] == '421':
# 421 SyniTox .PRIVMSG :Unknown command
pass
elif l[1] == '477':
#477 SyniTox #tor :Cannot join channel (Need to be identified and verified to join this channel, '/msg NickServ help' to learn how to register and verify.)
self.irc.send(bytes('HELP \r\n', 'UTF-8'))
self.irc.send(bytes('MSG NickServ help\r\n', 'UTF-8'))
pass
self.do()
except KeyboardInterrupt:
self.save_to_file('data')
ret = 0
except Exception as e:
LOG.exception(f'Error running program:\n{e}', exc_info=True)
ret = 1
else:
ret = 0
self.quit()
return ret
def quit(self):
self.del_callbacks()
self.save_to_file()
def save_to_file(self):
pass
def irc_send(self, msg):
success = False
while not success:
try:
self.irc.send(msg)
self.irc.send(bytes(msg, 'UTF-8'))
success = True
break
except socket.error:
@ -185,56 +450,57 @@ class SyncBot(Tox):
def on_connection_status(self, friendId, status):
if not self.request and not self.joined \
and friendId == self.bid and status:
print('Groupbot online, trying to join group chat.')
LOG.info('Groupbot online, trying to join group chat.')
self.request = True
self.ensure_exe(self.send_message, (self.bid, 'invite'))
self.ensure_exe(self.send_message, self.bid, 'invite')
def on_group_invite(self, friendid, type, data):
def on_group_invite(self, friendid, invite_data, user_data):
if not self.joined:
self.joined = True
self.tox_group_id = self.join_groupchat(friendid, data)
print('Joined groupchat.')
LOG.info('Joined groupchat.')
def on_group_message(self, groupnumber, friendgroupnumber, message):
name = self.group_peername(groupnumber, friendgroupnumber)
def on_group_message(self, groupnumber, peer_id, message):
name = self.group_peername(groupnumber, peer_id)
if len(name) and name != NAME:
print('TOX> %s: %s' % (name, message))
if message.startswith('>'):
message = '\x0309%s\x03' % message
self.irc_send('PRIVMSG %s :[%s]: %s\r\n' %
(CHANNEL, name, message))
self.irc_send(b'PRIVMSG %s :[%s]: %s\r\n' %
(self._sChannel, name, message))
if message.startswith('^'):
self.handle_command(message)
def on_group_action(self, groupnumber, friendgroupnumber, action):
name = self.group_peername(groupnumber, friendgroupnumber)
def on_group_action(self, groupnumber, peer_id, action):
"""old? message type action?"""
name = self.group_peername(groupnumber, peer_id)
if len(name) and name != NAME:
print('TOX> %s: %s' % (name, action))
if action.startswith('>'):
action = '\x0309%s\x03' % action
self.irc_send('PRIVMSG %s :\x01ACTION [%s]: %s\x01\r\n' %
(CHANNEL, name, action))
(self._sChannel, name, action))
def on_friend_request(self, pk, message):
print('Friend request from %s: %s' % (pk, message))
LOG.info('Friend request from %s: %s' % (pk, message))
self.add_friend_norequest(pk)
print('Accepted.')
LOG.info('Accepted.')
def on_friend_message(self, friendid, message):
if message == 'invite':
if not self.tox_group_id is None:
print('Inviting %s' % self.get_name(friendid))
LOG.info('Inviting %s' % self.get_name(friendid))
self.invite_friend(friendid, self.tox_group_id)
return
else:
message = 'Waiting for GroupBot, please try again in 1 min.'
self.ensure_exe(self.send_message, (friendid, message))
self.ensure_exe(self.send_message, friendid, message)
def send_both(self, content):
self.ensure_exe(self.group_message_send, (self.tox_group_id, content))
self.irc_send('PRIVMSG %s :%s\r\n' % (CHANNEL, content))
self.ensure_exe(self.group_message_send, self.tox_group_id, content)
self.irc_send('PRIVMSG %s :%s\r\n' % (self._sChannel, content))
def handle_command(self, cmd):
cmd = cmd[1:]
@ -247,12 +513,111 @@ class SyncBot(Tox):
subject = args[0]
desc = ' '.join(args[1:])
self.memory[subject] = desc
with open(MEMORY_DB, 'w') as f:
pickle.dump(self.memory, f)
if self.sMEMORY_DB:
with open(self.sMEMORY_DB, 'w') as f:
pickle.dump(self.memory, f)
self.send_both('Remembering ^%s: %s' % (subject, desc))
elif self.memory.has_key(cmd):
self.send_both(self.memory[cmd])
t = SyncBot()
t.loop()
def iMain(oArgs):
assert oTOX_OPTIONS
assert oTOX_OARGS
sChannel = oArgs.irc_chan
sIRC_HOST = oArgs.irc_host
iIRC_PORT = oArgs.irc_port
o = SyniTox(oTOX_OPTIONS, sChannel, sIRC_HOST, iIRC_PORT)
o.start()
ret = o.iLoop()
return ret
def oToxygenToxOptions(oArgs):
data = None
tox_options = wrapper.tox.Tox.options_new()
if oArgs.proxy_type:
tox_options.contents.proxy_type = int(oArgs.proxy_type)
tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8')
tox_options.contents.proxy_port = int(oArgs.proxy_port)
tox_options.contents.udp_enabled = False
else:
tox_options.contents.udp_enabled = oArgs.udp_enabled
if not os.path.exists('/proc/sys/net/ipv6'):
oArgs.ipv6_enabled = False
tox_options.contents.tcp_port = int(oArgs.tcp_port)
# overrides
tox_options.contents.local_discovery_enabled = False
tox_options.contents.dht_announcements_enabled = True
tox_options.contents.hole_punching_enabled = False
tox_options.contents.experimental_thread_safety = False
# REQUIRED!!
if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'):
LOG.warn('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.warn("No tox_options._options_pointer " +repr(tox_options._options_pointer))
return tox_options
def oArgparse(lArgv):
parser = ts.oMainArgparser()
parser.add_argument('profile', type=str, nargs='?', default=None,
help='Path to Tox profile')
# irc.libera.net #tox will not work over Tor
parser.add_argument('--irc_host', type=str, default='irc.oftc.net')
parser.add_argument('--irc_port', type=int, default=6667)
parser.add_argument('--irc_chan', type=str, default='#tor')
oArgs = parser.parse_args(lArgv)
for key in ts.lBOOLEANS:
if key not in oArgs: continue
val = getattr(oArgs, key)
setattr(oArgs, key, bool(val))
if hasattr(oArgs, 'sleep'):
if oArgs.sleep == 'qt':
pass # broken or gevent.sleep(idle_period)
elif oArgs.sleep == 'gevent':
pass # broken or gevent.sleep(idle_period)
else:
oArgs.sleep = 'time'
return oArgs
def main(lArgs=None):
global oTOX_OARGS
if lArgs is None: lArgs = []
oArgs = oArgparse(lArgs)
oTOX_OARGS = oArgs
global oTOX_OPTIONS
oTOX_OPTIONS = oToxygenToxOptions(oArgs)
ts.vSetupLogging(oArgs)
# ts.setup_logging(oArgs)
return iMain(oArgs)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
# Ran 34 tests in 86.589s OK (skipped=12)