|
|
|
|
@ -4,217 +4,198 @@ import base64
|
|
|
|
|
import random |
|
|
|
|
import socket |
|
|
|
|
import ssl |
|
|
|
|
#TODO: SSL support (can it be done without back-end openssl?) |
|
|
|
|
import threading |
|
|
|
|
import time |
|
|
|
|
import Queue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from twisted.internet import reactor, protocol |
|
|
|
|
from twisted.internet.endpoints import TCP4ClientEndpoint |
|
|
|
|
from twisted.internet.ssl import ClientContextFactory |
|
|
|
|
from twisted.logger import Logger |
|
|
|
|
from twisted.words.protocols import irc |
|
|
|
|
from jmdaemon.message_channel import MessageChannel |
|
|
|
|
from jmbase.support import get_log, chunks |
|
|
|
|
from jmdaemon.socks import socksocket, setdefaultproxy, PROXY_TYPE_SOCKS5 |
|
|
|
|
from txsocksx.client import SOCKS5ClientEndpoint |
|
|
|
|
from txsocksx.tls import TLSWrapClientEndpoint |
|
|
|
|
from jmdaemon.protocol import * |
|
|
|
|
MAX_PRIVMSG_LEN = 450 |
|
|
|
|
PING_INTERVAL = 300 |
|
|
|
|
PING_TIMEOUT = 60 |
|
|
|
|
|
|
|
|
|
#Throttling parameters; data from |
|
|
|
|
#tests by @chris-belcher: |
|
|
|
|
##worked (bytes per sec/bytes per sec interval / counterparties / max_privmsg_len) |
|
|
|
|
#300/4 / 6 / 400 |
|
|
|
|
#600/4 / 6 / 400 |
|
|
|
|
#450/4 / 10 / 400 |
|
|
|
|
#450/4 / 10 / 450 |
|
|
|
|
#525/4 / 10 / 450 |
|
|
|
|
##didnt work |
|
|
|
|
#600/4 / 10 / 450 |
|
|
|
|
#600/4 / 10 / 400 |
|
|
|
|
#2000/2 / 10 / 400 |
|
|
|
|
#450/4 / 10 / 475 |
|
|
|
|
MSG_INTERVAL = 0.001 |
|
|
|
|
B_PER_SEC = 450 |
|
|
|
|
B_PER_SEC_INTERVAL = 4.0 |
|
|
|
|
|
|
|
|
|
def get_config_irc_channel(chan_name, btcnet): |
|
|
|
|
channel = "#" + chan_name |
|
|
|
|
if btcnet == "testnet": |
|
|
|
|
channel += "-test" |
|
|
|
|
return channel |
|
|
|
|
|
|
|
|
|
log = get_log() |
|
|
|
|
|
|
|
|
|
def wlog(*x): |
|
|
|
|
"""Simplifier to add lists to the debug log |
|
|
|
|
""" |
|
|
|
|
msg = " ".join([str(a) for a in x]) |
|
|
|
|
log.debug(msg) |
|
|
|
|
|
|
|
|
|
def get_irc_text(line): |
|
|
|
|
return line[line[1:].find(':') + 2:] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_irc_nick(source): |
|
|
|
|
full_nick = source[1:source.find('!')] |
|
|
|
|
full_nick = source[0:source.find('!')] |
|
|
|
|
return full_nick[:NICK_MAX_ENCODED+2] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ThrottleThread(threading.Thread): |
|
|
|
|
def get_config_irc_channel(chan_name, btcnet): |
|
|
|
|
channel = "#" + chan_name |
|
|
|
|
if btcnet == "testnet": |
|
|
|
|
channel += "-test" |
|
|
|
|
return channel |
|
|
|
|
|
|
|
|
|
def __init__(self, irc): |
|
|
|
|
threading.Thread.__init__(self, name='ThrottleThread') |
|
|
|
|
self.daemon = True |
|
|
|
|
self.irc = irc |
|
|
|
|
self.msg_buffer = [] |
|
|
|
|
class TxIRCFactory(protocol.ClientFactory): |
|
|
|
|
def __init__(self, wrapper): |
|
|
|
|
self.wrapper = wrapper |
|
|
|
|
self.channel = self.wrapper.channel |
|
|
|
|
|
|
|
|
|
def run(self): |
|
|
|
|
log.debug("starting throttle thread") |
|
|
|
|
last_msg_time = 0 |
|
|
|
|
print_throttle_msg = True |
|
|
|
|
while not self.irc.give_up: |
|
|
|
|
self.irc.lockthrottle.acquire() |
|
|
|
|
while not (self.irc.throttleQ.empty() and self.irc.obQ.empty() |
|
|
|
|
and self.irc.pingQ.empty()): |
|
|
|
|
time.sleep(0.0001) #need to avoid cpu spinning if throttled |
|
|
|
|
try: |
|
|
|
|
pingmsg = self.irc.pingQ.get(block=False) |
|
|
|
|
#ping messages are not counted to throttling totals, |
|
|
|
|
#so send immediately |
|
|
|
|
self.irc.sock.sendall(pingmsg + '\r\n') |
|
|
|
|
continue |
|
|
|
|
except Queue.Empty: |
|
|
|
|
pass |
|
|
|
|
except: |
|
|
|
|
log.warn("failed to send ping message on socket") |
|
|
|
|
break |
|
|
|
|
#First throttling mechanism: no more than 1 line |
|
|
|
|
#per MSG_INTERVAL seconds. |
|
|
|
|
x = time.time() - last_msg_time |
|
|
|
|
if x < MSG_INTERVAL: |
|
|
|
|
continue |
|
|
|
|
#Second throttling mechanism: limited kB/s rate |
|
|
|
|
#over the most recent period. |
|
|
|
|
q = time.time() - B_PER_SEC_INTERVAL |
|
|
|
|
#clean out old messages |
|
|
|
|
self.msg_buffer = [_ for _ in self.msg_buffer if _[1] > q] |
|
|
|
|
bytes_recent = sum(len(i[0]) for i in self.msg_buffer) |
|
|
|
|
if bytes_recent > B_PER_SEC * B_PER_SEC_INTERVAL: |
|
|
|
|
if print_throttle_msg: |
|
|
|
|
log.debug("Throttling triggered, with: "+str( |
|
|
|
|
bytes_recent)+ " bytes in the last "+str( |
|
|
|
|
B_PER_SEC_INTERVAL)+" seconds.") |
|
|
|
|
print_throttle_msg = False |
|
|
|
|
continue |
|
|
|
|
print_throttle_msg = True |
|
|
|
|
try: |
|
|
|
|
throttled_msg = self.irc.throttleQ.get(block=False) |
|
|
|
|
except Queue.Empty: |
|
|
|
|
try: |
|
|
|
|
throttled_msg = self.irc.obQ.get(block=False) |
|
|
|
|
except Queue.Empty: |
|
|
|
|
#this code *should* be unreachable. |
|
|
|
|
continue |
|
|
|
|
try: |
|
|
|
|
self.irc.sock.sendall(throttled_msg+'\r\n') |
|
|
|
|
last_msg_time = time.time() |
|
|
|
|
self.msg_buffer.append((throttled_msg, last_msg_time)) |
|
|
|
|
except: |
|
|
|
|
log.error("failed to send on socket") |
|
|
|
|
try: |
|
|
|
|
self.irc.fd.close() |
|
|
|
|
except: pass |
|
|
|
|
break |
|
|
|
|
self.irc.lockthrottle.wait() |
|
|
|
|
self.irc.lockthrottle.release() |
|
|
|
|
|
|
|
|
|
log.debug("Ended throttling thread.") |
|
|
|
|
|
|
|
|
|
class PingThread(threading.Thread): |
|
|
|
|
|
|
|
|
|
def __init__(self, irc): |
|
|
|
|
threading.Thread.__init__(self, name='PingThread') |
|
|
|
|
self.daemon = True |
|
|
|
|
self.irc = irc |
|
|
|
|
def buildProtocol(self, addr): |
|
|
|
|
p = txIRC_Client(self.wrapper) |
|
|
|
|
p.factory = self |
|
|
|
|
self.wrapper.set_tx_irc_client(p) |
|
|
|
|
return p |
|
|
|
|
|
|
|
|
|
def clientConnectionLost(self, connector, reason): |
|
|
|
|
log.info('IRC connection lost: ' + str(reason)) |
|
|
|
|
if not self.wrapper.give_up: |
|
|
|
|
log.info('Attempting to reconnect...') |
|
|
|
|
reactor.callLater(self.wrapper.reconnect_interval, |
|
|
|
|
connector.connect()) |
|
|
|
|
|
|
|
|
|
def clientConnectionFailed(self, connector, reason): |
|
|
|
|
log.info('IRC connection failed: ' + reason) |
|
|
|
|
|
|
|
|
|
def run(self): |
|
|
|
|
log.debug('starting ping thread') |
|
|
|
|
while not self.irc.give_up: |
|
|
|
|
time.sleep(PING_INTERVAL) |
|
|
|
|
try: |
|
|
|
|
self.irc.ping_reply = False |
|
|
|
|
# maybe use this to calculate the lag one day |
|
|
|
|
self.irc.lockcond.acquire() |
|
|
|
|
self.irc.send_raw('PING LAG' + str(int(time.time() * 1000))) |
|
|
|
|
self.irc.lockcond.wait(PING_TIMEOUT) |
|
|
|
|
self.irc.lockcond.release() |
|
|
|
|
if not self.irc.ping_reply: |
|
|
|
|
log.warn('irc ping timed out') |
|
|
|
|
try: |
|
|
|
|
self.irc.close() |
|
|
|
|
except: |
|
|
|
|
pass |
|
|
|
|
try: |
|
|
|
|
self.irc.fd.close() |
|
|
|
|
except: |
|
|
|
|
pass |
|
|
|
|
try: |
|
|
|
|
self.irc.sock.shutdown(socket.SHUT_RDWR) |
|
|
|
|
self.irc.sock.close() |
|
|
|
|
except: |
|
|
|
|
pass |
|
|
|
|
except IOError as e: |
|
|
|
|
log.debug('ping thread: ' + repr(e)) |
|
|
|
|
log.debug('ended ping thread') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# handle one channel at a time |
|
|
|
|
class IRCMessageChannel(MessageChannel): |
|
|
|
|
# close implies it will attempt to reconnect |
|
|
|
|
def close(self): |
|
|
|
|
try: |
|
|
|
|
self.sock.sendall("QUIT\r\n") |
|
|
|
|
except IOError as e: |
|
|
|
|
log.info('errored while trying to quit: ' + repr(e)) |
|
|
|
|
|
|
|
|
|
def __init__(self, |
|
|
|
|
configdata, |
|
|
|
|
username='username', |
|
|
|
|
realname='realname', |
|
|
|
|
password=None, |
|
|
|
|
daemon=None): |
|
|
|
|
MessageChannel.__init__(self, daemon=daemon) |
|
|
|
|
self.give_up = True |
|
|
|
|
self.serverport = (configdata['host'], configdata['port']) |
|
|
|
|
#default hostid for use with miniircd which doesnt send NETWORK |
|
|
|
|
self.hostid = configdata['host'] + str(configdata['port']) |
|
|
|
|
self.socks5 = configdata["socks5"] |
|
|
|
|
self.usessl = configdata["usessl"] |
|
|
|
|
self.socks5_host = configdata["socks5_host"] |
|
|
|
|
self.socks5_port = int(configdata["socks5_port"]) |
|
|
|
|
self.channel = get_config_irc_channel(configdata["channel"], |
|
|
|
|
configdata["btcnet"]) |
|
|
|
|
self.userrealname = (username, realname) |
|
|
|
|
if password and len(password) == 0: |
|
|
|
|
password = None |
|
|
|
|
self.password = password |
|
|
|
|
|
|
|
|
|
self.tx_irc_client = None |
|
|
|
|
#TODO can be configuration var, how long between reconnect attempts: |
|
|
|
|
self.reconnect_interval = 10 |
|
|
|
|
#implementation of abstract base class methods; |
|
|
|
|
#these are mostly but not exclusively acting as pass through |
|
|
|
|
#to the wrapped twisted IRC client protocol |
|
|
|
|
def run(self): |
|
|
|
|
self.give_up = False |
|
|
|
|
self.build_irc() |
|
|
|
|
|
|
|
|
|
def shutdown(self): |
|
|
|
|
self.close() |
|
|
|
|
self.tx_irc_client.quit() |
|
|
|
|
self.give_up = True |
|
|
|
|
|
|
|
|
|
# Maker callbacks |
|
|
|
|
def _announce_orders(self, orderlist): |
|
|
|
|
"""This publishes orders to the pit and to |
|
|
|
|
counterparties. Note that it does *not* use chunking. |
|
|
|
|
So, it tries to optimise space usage thusly: |
|
|
|
|
As many complete orderlines are fit onto one line |
|
|
|
|
as possible, and overflow goes onto another line. |
|
|
|
|
Each list entry in orderlist must have format: |
|
|
|
|
!ordername <parameters> |
|
|
|
|
def _pubmsg(self, msg): |
|
|
|
|
self.tx_irc_client._pubmsg(msg) |
|
|
|
|
|
|
|
|
|
Then, what is published is lines of form: |
|
|
|
|
!ordername <parameters>!ordername <parameters>.. |
|
|
|
|
def _privmsg(self, nick, cmd, msg): |
|
|
|
|
self.tx_irc_client._privmsg(nick, cmd, msg) |
|
|
|
|
|
|
|
|
|
fitting as many list entries as possible onto one line, |
|
|
|
|
up to the limit of the IRC parameters (see MAX_PRIVMSG_LEN). |
|
|
|
|
def change_nick(self, new_nick): |
|
|
|
|
self.tx_irc_client.setNick(new_nick) |
|
|
|
|
|
|
|
|
|
Order announce in private is handled by privmsg/_privmsg |
|
|
|
|
using chunking, no longer using this function. |
|
|
|
|
def _announce_orders(self, offerlist): |
|
|
|
|
self.tx_irc_client._announce_orders(offerlist) |
|
|
|
|
#end ABC impl. |
|
|
|
|
|
|
|
|
|
def set_tx_irc_client(self, txircclt): |
|
|
|
|
self.tx_irc_client = txircclt |
|
|
|
|
|
|
|
|
|
def build_irc(self): |
|
|
|
|
"""The main starting method that creates a protocol object |
|
|
|
|
according to the config variables, ready for whenever |
|
|
|
|
the reactor starts running. |
|
|
|
|
""" |
|
|
|
|
header = 'PRIVMSG ' + self.channel + ' :' |
|
|
|
|
orderlines = [] |
|
|
|
|
for i, order in enumerate(orderlist): |
|
|
|
|
orderlines.append(order) |
|
|
|
|
line = header + ''.join(orderlines) + ' ~' |
|
|
|
|
if len(line) > MAX_PRIVMSG_LEN or i == len(orderlist) - 1: |
|
|
|
|
if i < len(orderlist) - 1: |
|
|
|
|
line = header + ''.join(orderlines[:-1]) + ' ~' |
|
|
|
|
self.send_raw(line) |
|
|
|
|
orderlines = [orderlines[-1]] |
|
|
|
|
wlog('building irc') |
|
|
|
|
if self.tx_irc_client: |
|
|
|
|
raise Exception('irc already built') |
|
|
|
|
if self.usessl.lower() == 'true': |
|
|
|
|
factory = TxIRCFactory(self) |
|
|
|
|
ctx = ClientContextFactory() |
|
|
|
|
reactor.connectSSL(self.serverport[0], self.serverport[1], |
|
|
|
|
factory, ctx) |
|
|
|
|
elif self.socks5.lower() == 'true': |
|
|
|
|
#TODO not yet tested! to say it needs to be is a slight understatement. |
|
|
|
|
factory = TxIRCFactory(self) |
|
|
|
|
torEndpoint = TCP4ClientEndpoint(reactor, self.socks5_host, |
|
|
|
|
self.socks5_port) |
|
|
|
|
ircEndpoint = SOCKS5ClientEndpoint(self.serverport[0], |
|
|
|
|
self.serverport[1], torEndpoint) |
|
|
|
|
if self.usessl: |
|
|
|
|
ctx = ClientContextFactory() |
|
|
|
|
tlsEndpoint = TLSWrapClientEndpoint(ctx, ircEndpoint) |
|
|
|
|
tlsEndpoint.connect(factory) |
|
|
|
|
else: |
|
|
|
|
ircEndpoint.connect(factory) |
|
|
|
|
else: |
|
|
|
|
try: |
|
|
|
|
factory = TxIRCFactory(self) |
|
|
|
|
wlog('build_irc: ', self.serverport[0], self.serverport[1], |
|
|
|
|
self.channel) |
|
|
|
|
self.tcp_connector = reactor.connectTCP( |
|
|
|
|
self.serverport[0], self.serverport[1], factory) |
|
|
|
|
except Exception as e: |
|
|
|
|
wlog('error in buildirc: ' + repr(e)) |
|
|
|
|
|
|
|
|
|
class txIRC_Client(irc.IRCClient, object): |
|
|
|
|
""" |
|
|
|
|
lineRate is a class variable in the superclass used to limit |
|
|
|
|
messages / second. heartbeat is what you'd think |
|
|
|
|
TODO check this handles throttling as necessary, should do. |
|
|
|
|
""" |
|
|
|
|
lineRate = 0.5 |
|
|
|
|
heartbeatinterval = 60 |
|
|
|
|
|
|
|
|
|
def __init__(self, wrapper): |
|
|
|
|
self.wrapper = wrapper |
|
|
|
|
self.channel = self.wrapper.channel |
|
|
|
|
self.nickname = self.wrapper.nick |
|
|
|
|
self.password = self.wrapper.password |
|
|
|
|
self.hostname = self.wrapper.serverport[0] |
|
|
|
|
self.built_privmsg = {} |
|
|
|
|
# todo: build pong timeout watchdot |
|
|
|
|
|
|
|
|
|
def irc_unknown(self, prefix, command, params): |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
def irc_PONG(self, *args, **kwargs): |
|
|
|
|
# todo: pong called getattr() style. use for health |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
def connectionMade(self): |
|
|
|
|
return irc.IRCClient.connectionMade(self) |
|
|
|
|
|
|
|
|
|
def connectionLost(self, reason=protocol.connectionDone): |
|
|
|
|
wlog('connectionLost:') |
|
|
|
|
if self.wrapper.on_disconnect: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_disconnect, self.wrapper) |
|
|
|
|
return irc.IRCClient.connectionLost(self, reason) |
|
|
|
|
|
|
|
|
|
def send(self, send_to, msg): |
|
|
|
|
# todo: use proper twisted IRC support (encoding + sendCommand) |
|
|
|
|
omsg = 'PRIVMSG %s :' % (send_to,) + msg |
|
|
|
|
self.sendLine(omsg.encode('ascii')) |
|
|
|
|
|
|
|
|
|
def _pubmsg(self, message): |
|
|
|
|
line = "PRIVMSG " + self.channel + " :" + message |
|
|
|
|
assert len(line) <= MAX_PRIVMSG_LEN |
|
|
|
|
ob = False |
|
|
|
|
if any([x in line for x in offername_list]): |
|
|
|
|
ob = True |
|
|
|
|
self.send_raw(line, ob) |
|
|
|
|
self.send(self.channel, message) |
|
|
|
|
|
|
|
|
|
def _privmsg(self, nick, cmd, message): |
|
|
|
|
"""Send a privmsg to an irc counterparty, |
|
|
|
|
using chunking as appropriate for long messages. |
|
|
|
|
""" |
|
|
|
|
ob = True if cmd in offername_list else False |
|
|
|
|
header = "PRIVMSG " + nick + " :" |
|
|
|
|
max_chunk_len = MAX_PRIVMSG_LEN - len(header) - len(cmd) - 4 |
|
|
|
|
# 1 for command prefix 1 for space 2 for trailer |
|
|
|
|
@ -226,253 +207,174 @@ class IRCMessageChannel(MessageChannel):
|
|
|
|
|
trailer = ' ~' if m == message_chunks[-1] else ' ;' |
|
|
|
|
if m == message_chunks[0]: |
|
|
|
|
m = COMMAND_PREFIX + cmd + ' ' + m |
|
|
|
|
self.send_raw(header + m + trailer, ob) |
|
|
|
|
self.send(nick, m + trailer) |
|
|
|
|
|
|
|
|
|
def change_nick(self, new_nick): |
|
|
|
|
self.nick = new_nick |
|
|
|
|
self.send_raw('NICK ' + self.nick) |
|
|
|
|
|
|
|
|
|
def send_raw(self, line, ob=False): |
|
|
|
|
# Messages are queued and prioritised. |
|
|
|
|
# This is an addressing of github #300 |
|
|
|
|
if line.startswith("PING") or line.startswith("PONG"): |
|
|
|
|
self.pingQ.put(line) |
|
|
|
|
elif ob: |
|
|
|
|
self.obQ.put(line) |
|
|
|
|
else: |
|
|
|
|
self.throttleQ.put(line) |
|
|
|
|
self.lockthrottle.acquire() |
|
|
|
|
self.lockthrottle.notify() |
|
|
|
|
self.lockthrottle.release() |
|
|
|
|
|
|
|
|
|
def __handle_privmsg(self, source, target, message): |
|
|
|
|
nick = get_irc_nick(source) |
|
|
|
|
#ensure return value 'parsed' is length > 2 |
|
|
|
|
if len(message) < 4: |
|
|
|
|
return |
|
|
|
|
if target == self.nick: |
|
|
|
|
if message[0] == '\x01': |
|
|
|
|
endindex = message[1:].find('\x01') |
|
|
|
|
if endindex == -1: |
|
|
|
|
return |
|
|
|
|
ctcp = message[1:endindex + 1] |
|
|
|
|
if ctcp.upper() == 'VERSION': |
|
|
|
|
self.send_raw('PRIVMSG ' + nick + |
|
|
|
|
' :\x01VERSION xchat 2.8.8 Ubuntu\x01') |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
if nick not in self.built_privmsg: |
|
|
|
|
self.built_privmsg[nick] = message[:-2] |
|
|
|
|
else: |
|
|
|
|
self.built_privmsg[nick] += message[:-2] |
|
|
|
|
if message[-1] == '~': |
|
|
|
|
parsed = self.built_privmsg[nick] |
|
|
|
|
# wipe the message buffer waiting for the next one |
|
|
|
|
del self.built_privmsg[nick] |
|
|
|
|
log.debug("<<privmsg on %s: " % |
|
|
|
|
(self.hostid) + "nick=%s message=%s" % (nick, parsed)) |
|
|
|
|
self.on_privmsg(nick, parsed) |
|
|
|
|
elif message[-1] != ';': |
|
|
|
|
# drop the bad nick |
|
|
|
|
del self.built_privmsg[nick] |
|
|
|
|
elif target == self.channel: |
|
|
|
|
log.info("<<pubmsg on %s: " % |
|
|
|
|
(self.hostid) + "nick=%s message=%s" % |
|
|
|
|
(nick, message)) |
|
|
|
|
self.on_pubmsg(nick, message) |
|
|
|
|
else: |
|
|
|
|
log.debug("what is this? privmsg on %s: " % |
|
|
|
|
(self.hostid) + "src=%s target=%s message=%s;" % |
|
|
|
|
(source, target, message)) |
|
|
|
|
|
|
|
|
|
def __handle_line(self, line): |
|
|
|
|
line = line.rstrip() |
|
|
|
|
# log.debug('<< ' + line) |
|
|
|
|
if line.startswith('PING '): |
|
|
|
|
self.send_raw(line.replace('PING', 'PONG')) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
_chunks = line.split(' ') |
|
|
|
|
if _chunks[1] == 'QUIT': |
|
|
|
|
nick = get_irc_nick(_chunks[0]) |
|
|
|
|
if nick == self.nick: |
|
|
|
|
raise IOError('we quit') |
|
|
|
|
else: |
|
|
|
|
if self.on_nick_leave: |
|
|
|
|
self.on_nick_leave(nick, self) |
|
|
|
|
elif _chunks[1] == '433': # nick in use |
|
|
|
|
# helps keep identity constant if just _ added |
|
|
|
|
#request new nick on *all* channels via callback |
|
|
|
|
if self.on_nick_change: |
|
|
|
|
self.on_nick_change(self.nick + '_') |
|
|
|
|
if self.password: |
|
|
|
|
if _chunks[1] == 'CAP': |
|
|
|
|
if _chunks[3] != 'ACK': |
|
|
|
|
log.warn("server %s " % |
|
|
|
|
(self.hostid) + "does not support SASL, quitting") |
|
|
|
|
self.shutdown() |
|
|
|
|
self.send_raw('AUTHENTICATE PLAIN') |
|
|
|
|
elif _chunks[0] == 'AUTHENTICATE': |
|
|
|
|
self.send_raw('AUTHENTICATE ' + base64.b64encode( |
|
|
|
|
self.nick + '\x00' + self.nick + '\x00' + self.password)) |
|
|
|
|
elif _chunks[1] == '903': |
|
|
|
|
log.info("Successfully authenticated on %s" % |
|
|
|
|
(self.hostid)) |
|
|
|
|
self.password = None |
|
|
|
|
self.send_raw('CAP END') |
|
|
|
|
elif _chunks[1] == '904': |
|
|
|
|
log.warn("Failed authentication %s " % |
|
|
|
|
(self.hostid) + ", wrong password") |
|
|
|
|
self.shutdown() |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
if _chunks[1] == 'PRIVMSG': |
|
|
|
|
self.__handle_privmsg(_chunks[0], _chunks[2], get_irc_text(line)) |
|
|
|
|
if _chunks[1] == 'PONG': |
|
|
|
|
self.ping_reply = True |
|
|
|
|
self.lockcond.acquire() |
|
|
|
|
self.lockcond.notify() |
|
|
|
|
self.lockcond.release() |
|
|
|
|
elif _chunks[1] == '376': # end of motd |
|
|
|
|
self.built_privmsg = {} |
|
|
|
|
if self.on_connect: |
|
|
|
|
self.on_connect(self) |
|
|
|
|
if self.hostid == 'agora-irc': |
|
|
|
|
self.send_raw('PART #AGORA') |
|
|
|
|
self.send_raw('JOIN ' + self.channel) |
|
|
|
|
self.send_raw( |
|
|
|
|
'MODE ' + self.nick + ' +B') # marks as bots on unreal |
|
|
|
|
self.send_raw( |
|
|
|
|
'MODE ' + self.nick + ' -R') # allows unreg'd private messages |
|
|
|
|
elif _chunks[1] == '366': # end of names list |
|
|
|
|
log.info("Connected to IRC and joined channel on %s " % |
|
|
|
|
(self.hostid)) |
|
|
|
|
if self.on_welcome: |
|
|
|
|
self.on_welcome(self) #informs mc-collection that we are ready for use |
|
|
|
|
elif _chunks[1] == '332' or _chunks[1] == 'TOPIC': # channel topic |
|
|
|
|
topic = get_irc_text(line) |
|
|
|
|
self.on_set_topic(topic) |
|
|
|
|
elif _chunks[1] == 'KICK': |
|
|
|
|
target = _chunks[3] |
|
|
|
|
if target == self.nick: |
|
|
|
|
self.give_up = True |
|
|
|
|
fmt = '{} has kicked us from the irc channel! Reason= {}'.format |
|
|
|
|
raise IOError(fmt(get_irc_nick(_chunks[0]), get_irc_text(line))) |
|
|
|
|
else: |
|
|
|
|
if self.on_nick_leave: |
|
|
|
|
self.on_nick_leave(target, self) |
|
|
|
|
elif _chunks[1] == 'PART': |
|
|
|
|
nick = get_irc_nick(_chunks[0]) |
|
|
|
|
if self.on_nick_leave: |
|
|
|
|
self.on_nick_leave(nick, self) |
|
|
|
|
elif _chunks[1] == '005': |
|
|
|
|
''' |
|
|
|
|
:port80b.se.quakenet.org 005 J5BzJGGfyw5GaPc MAXNICKLEN=15 |
|
|
|
|
TOPICLEN=250 AWAYLEN=160 KICKLEN=250 CHANNELLEN=200 |
|
|
|
|
MAXCHANNELLEN=200 CHANTYPES=#& PREFIX=(ov)@+ STATUSMSG=@+ |
|
|
|
|
CHANMODES=b,k,l,imnpstrDducCNMT CASEMAPPING=rfc1459 |
|
|
|
|
NETWORK=QuakeNet :are supported by this server |
|
|
|
|
''' |
|
|
|
|
for chu in _chunks[3:]: |
|
|
|
|
if chu[0] == ':': |
|
|
|
|
break |
|
|
|
|
if chu.lower().startswith('network='): |
|
|
|
|
self.hostid = chu[8:] |
|
|
|
|
log.debug('found network name: ' + self.hostid + ';') |
|
|
|
|
def _announce_orders(self, offerlist): |
|
|
|
|
"""This publishes orders to the pit and to |
|
|
|
|
counterparties. Note that it does *not* use chunking. |
|
|
|
|
So, it tries to optimise space usage thusly: |
|
|
|
|
As many complete orderlines are fit onto one line |
|
|
|
|
as possible, and overflow goes onto another line. |
|
|
|
|
Each list entry in orderlist must have format: |
|
|
|
|
!ordername <parameters> |
|
|
|
|
|
|
|
|
|
def __init__(self, |
|
|
|
|
configdata, |
|
|
|
|
username='username', |
|
|
|
|
realname='realname', |
|
|
|
|
password=None, |
|
|
|
|
daemon=None): |
|
|
|
|
MessageChannel.__init__(self, daemon=daemon) |
|
|
|
|
self.give_up = True |
|
|
|
|
self.serverport = (configdata['host'], configdata['port']) |
|
|
|
|
#default hostid for use with miniircd which doesnt send NETWORK |
|
|
|
|
self.hostid = configdata['host'] + str(configdata['port']) |
|
|
|
|
self.socks5 = configdata["socks5"] |
|
|
|
|
self.usessl = configdata["usessl"] |
|
|
|
|
self.socks5_host = configdata["socks5_host"] |
|
|
|
|
self.socks5_port = int(configdata["socks5_port"]) |
|
|
|
|
self.channel = get_config_irc_channel(configdata["channel"], |
|
|
|
|
configdata["btcnet"]) |
|
|
|
|
self.userrealname = (username, realname) |
|
|
|
|
if password and len(password) == 0: |
|
|
|
|
password = None |
|
|
|
|
self.given_password = password |
|
|
|
|
self.pingQ = Queue.Queue() |
|
|
|
|
self.throttleQ = Queue.Queue() |
|
|
|
|
self.obQ = Queue.Queue() |
|
|
|
|
self.reconnect_interval = 30 |
|
|
|
|
|
|
|
|
|
def set_reconnect_interval(self, interval): |
|
|
|
|
"""For testing reconnection functions. |
|
|
|
|
""" |
|
|
|
|
self.reconnect_interval = interval |
|
|
|
|
Then, what is published is lines of form: |
|
|
|
|
!ordername <parameters>!ordername <parameters>.. |
|
|
|
|
|
|
|
|
|
def run(self): |
|
|
|
|
self.give_up = False |
|
|
|
|
self.ping_reply = True |
|
|
|
|
self.lockcond = threading.Condition() |
|
|
|
|
self.lockthrottle = threading.Condition() |
|
|
|
|
PingThread(self).start() |
|
|
|
|
ThrottleThread(self).start() |
|
|
|
|
fitting as many list entries as possible onto one line, |
|
|
|
|
up to the limit of the IRC parameters (see MAX_PRIVMSG_LEN). |
|
|
|
|
|
|
|
|
|
while not self.give_up: |
|
|
|
|
try: |
|
|
|
|
log.info("connecting to host %s" % |
|
|
|
|
(self.hostid)) |
|
|
|
|
if self.socks5.lower() == 'true': |
|
|
|
|
log.debug("Using socks5 proxy %s:%d" % |
|
|
|
|
(self.socks5_host, self.socks5_port)) |
|
|
|
|
setdefaultproxy(PROXY_TYPE_SOCKS5, |
|
|
|
|
self.socks5_host, self.socks5_port, |
|
|
|
|
True) |
|
|
|
|
self.sock = socksocket() |
|
|
|
|
Order announce in private is handled by privmsg/_privmsg |
|
|
|
|
using chunking, no longer using this function. |
|
|
|
|
""" |
|
|
|
|
header = 'PRIVMSG ' + self.channel + ' :' |
|
|
|
|
offerlines = [] |
|
|
|
|
for i, offer in enumerate(offerlist): |
|
|
|
|
offerlines.append(offer) |
|
|
|
|
line = header + ''.join(offerlines) + ' ~' |
|
|
|
|
if len(line) > MAX_PRIVMSG_LEN or i == len(offerlist) - 1: |
|
|
|
|
if i < len(offerlist) - 1: |
|
|
|
|
line = header + ''.join(offerlines[:-1]) + ' ~' |
|
|
|
|
self.sendLine(line) |
|
|
|
|
offerlines = [offerlines[-1]] |
|
|
|
|
# --------------------------------------------- |
|
|
|
|
# general callbacks from superclass |
|
|
|
|
# --------------------------------------------- |
|
|
|
|
|
|
|
|
|
def signedOn(self): |
|
|
|
|
wlog('signedOn:') |
|
|
|
|
self.join(self.factory.channel) |
|
|
|
|
|
|
|
|
|
def joined(self, channel): |
|
|
|
|
wlog('joined: ', channel) |
|
|
|
|
#Use as trigger for start to mcc: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_welcome, self.wrapper) |
|
|
|
|
|
|
|
|
|
def privmsg(self, userIn, channel, msg): |
|
|
|
|
reactor.callLater(0.0, self.handle_privmsg, |
|
|
|
|
userIn, channel, msg) |
|
|
|
|
|
|
|
|
|
def __on_privmsg(self, nick, msg): |
|
|
|
|
self.wrapper.on_privmsg(nick, msg) |
|
|
|
|
|
|
|
|
|
def __on_pubmsg(self, nick, msg): |
|
|
|
|
self.wrapper.on_pubmsg(nick, msg) |
|
|
|
|
|
|
|
|
|
def handle_privmsg(self, sent_from, sent_to, message): |
|
|
|
|
try: |
|
|
|
|
nick = get_irc_nick(sent_from) |
|
|
|
|
# todo: kludge - we need this elsewhere. rearchitect!! |
|
|
|
|
self.from_to = (nick, sent_to) |
|
|
|
|
if sent_to == self.wrapper.nick: |
|
|
|
|
if nick not in self.built_privmsg: |
|
|
|
|
if message[0] != COMMAND_PREFIX: |
|
|
|
|
wlog('bad command ', message[0]) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# new message starting |
|
|
|
|
cmd_string = message[1:].split(' ')[0] |
|
|
|
|
self.built_privmsg[nick] = [cmd_string, message[:-2]] |
|
|
|
|
else: |
|
|
|
|
self.sock = socket.socket(socket.AF_INET, |
|
|
|
|
socket.SOCK_STREAM) |
|
|
|
|
self.sock.connect(self.serverport) |
|
|
|
|
if self.usessl.lower() == 'true': |
|
|
|
|
self.sock = ssl.wrap_socket(self.sock) |
|
|
|
|
self.fd = self.sock.makefile() |
|
|
|
|
self.password = None |
|
|
|
|
if self.given_password: |
|
|
|
|
self.password = self.given_password |
|
|
|
|
self.send_raw('CAP REQ :sasl') |
|
|
|
|
self.send_raw('USER %s b c :%s' % self.userrealname) |
|
|
|
|
self.nick = self.given_nick |
|
|
|
|
self.send_raw('NICK ' + self.nick) |
|
|
|
|
while 1: |
|
|
|
|
try: |
|
|
|
|
line = self.fd.readline() |
|
|
|
|
except AttributeError as e: |
|
|
|
|
raise IOError(repr(e)) |
|
|
|
|
if line is None: |
|
|
|
|
log.debug("line returned null from %s" % |
|
|
|
|
(self.hostid)) |
|
|
|
|
break |
|
|
|
|
if len(line) == 0: |
|
|
|
|
log.debug("line was zero length from %s" % |
|
|
|
|
(self.hostid)) |
|
|
|
|
break |
|
|
|
|
self.__handle_line(line) |
|
|
|
|
except IOError as e: |
|
|
|
|
import traceback |
|
|
|
|
log.debug("logging traceback from %s: \n" % |
|
|
|
|
(self.hostid) + traceback.format_exc()) |
|
|
|
|
finally: |
|
|
|
|
try: |
|
|
|
|
self.fd.close() |
|
|
|
|
self.sock.close() |
|
|
|
|
except Exception as e: |
|
|
|
|
self.built_privmsg[nick][1] += message[:-2] |
|
|
|
|
if message[-1] == ';': |
|
|
|
|
pass |
|
|
|
|
if self.on_disconnect: |
|
|
|
|
self.on_disconnect(self) |
|
|
|
|
log.info("disconnected from irc host %s" % |
|
|
|
|
(self.hostid)) |
|
|
|
|
if not self.give_up: |
|
|
|
|
time.sleep(self.reconnect_interval) |
|
|
|
|
log.info('ending irc') |
|
|
|
|
self.give_up = True |
|
|
|
|
elif message[-1] == '~': |
|
|
|
|
parsed = self.built_privmsg[nick][1] |
|
|
|
|
# wipe the message buffer waiting for the next one |
|
|
|
|
del self.built_privmsg[nick] |
|
|
|
|
self.__on_privmsg(nick, parsed) |
|
|
|
|
else: |
|
|
|
|
# drop the bad nick |
|
|
|
|
del self.built_privmsg[nick] |
|
|
|
|
elif sent_to == self.channel: |
|
|
|
|
self.__on_pubmsg(nick, message) |
|
|
|
|
else: |
|
|
|
|
wlog('what is this?: ', sent_from, sent_to, message[:80]) |
|
|
|
|
except: |
|
|
|
|
wlog('unable to parse privmsg, msg: ', message) |
|
|
|
|
|
|
|
|
|
def action(self, user, channel, msg): |
|
|
|
|
wlog('unhandled action: ', user, channel, msg) |
|
|
|
|
|
|
|
|
|
def alterCollidedNick(self, nickname): |
|
|
|
|
""" |
|
|
|
|
Generate an altered version of a nickname that caused a collision in an |
|
|
|
|
effort to create an unused related name for subsequent registration. |
|
|
|
|
:param nickname: |
|
|
|
|
""" |
|
|
|
|
newnick = nickname + '_' |
|
|
|
|
wlog('nickname collision, changed to ', newnick) |
|
|
|
|
return newnick |
|
|
|
|
|
|
|
|
|
def modeChanged(self, user, channel, _set, modes, args): |
|
|
|
|
wlog('(unhandled) modeChanged: ', user, channel, _set, modes, args) |
|
|
|
|
|
|
|
|
|
def pong(self, user, secs): |
|
|
|
|
wlog('pong: ', user, secs) |
|
|
|
|
|
|
|
|
|
def userJoined(self, user, channel): |
|
|
|
|
wlog('user joined: ', user, channel) |
|
|
|
|
|
|
|
|
|
def userKicked(self, kickee, channel, kicker, message): |
|
|
|
|
wlog('kicked: ', kickee, channel, kicker, message) |
|
|
|
|
if self.wrapper.on_nick_leave: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_nick_leave, kickee, self.wrapper) |
|
|
|
|
|
|
|
|
|
def userLeft(self, user, channel): |
|
|
|
|
wlog('left: ', user, channel) |
|
|
|
|
if self.wrapper.on_nick_leave: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_nick_leave, user, self.wrapper) |
|
|
|
|
|
|
|
|
|
def userRenamed(self, oldname, newname): |
|
|
|
|
wlog('rename: ', oldname, newname) |
|
|
|
|
#TODO nick change handling |
|
|
|
|
|
|
|
|
|
def userQuit(self, user, quitMessage): |
|
|
|
|
wlog('userQuit: ', user, quitMessage) |
|
|
|
|
if self.wrapper.on_nick_leave: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_nick_leave, user, self.wrapper) |
|
|
|
|
|
|
|
|
|
def topicUpdated(self, user, channel, newTopic): |
|
|
|
|
wlog('topicUpdated: ', user, channel, newTopic) |
|
|
|
|
if self.wrapper.on_set_topic: |
|
|
|
|
reactor.callLater(0.0, self.wrapper.on_set_topic, newTopic) |
|
|
|
|
|
|
|
|
|
def receivedMOTD(self, motd): |
|
|
|
|
wlog('motd: ', motd) |
|
|
|
|
|
|
|
|
|
def created(self, when): |
|
|
|
|
wlog('(unhandled) created: ', when) |
|
|
|
|
|
|
|
|
|
def yourHost(self, info): |
|
|
|
|
wlog('(unhandled) yourhost: ', info) |
|
|
|
|
|
|
|
|
|
def isupport(self, options): |
|
|
|
|
"""Used to set the name of the IRC *network* |
|
|
|
|
(as distinct from the individual server), used |
|
|
|
|
for signature replay defence (see signing code in message_channel.py). |
|
|
|
|
If this option ("NETWORK") is not found, we fallback to the default |
|
|
|
|
hostid = servername+port as shown in IRCMessageChannel (should only |
|
|
|
|
happen in testing). |
|
|
|
|
""" |
|
|
|
|
for o in options: |
|
|
|
|
try: |
|
|
|
|
k, v = o.split('=') |
|
|
|
|
if k == 'NETWORK': |
|
|
|
|
self.wrapper.hostid = v |
|
|
|
|
except Exception as e: |
|
|
|
|
wlog('failed to parse isupport option, ignoring') |
|
|
|
|
|
|
|
|
|
def myInfo(self, servername, version, umodes, cmodes): |
|
|
|
|
wlog('(unhandled) myInfo: ', servername, version, umodes, cmodes) |
|
|
|
|
|
|
|
|
|
def luserChannels(self, channels): |
|
|
|
|
wlog('(unhandled) luserChannels: ', channels) |
|
|
|
|
|
|
|
|
|
def bounce(self, info): |
|
|
|
|
wlog('(unhandled) bounce: ', info) |
|
|
|
|
|
|
|
|
|
def left(self, channel): |
|
|
|
|
wlog('(unhandled) left: ', channel) |
|
|
|
|
|
|
|
|
|
def noticed(self, user, channel, message): |
|
|
|
|
wlog('(unhandled) noticed: ', user, channel, message) |