from __future__ import absolute_import, print_function import base64 import random import socket import ssl import threading import time import Queue from jmdaemon.message_channel import MessageChannel from jmbase.support import get_log, chunks from jmdaemon.socks import socksocket, setdefaultproxy, PROXY_TYPE_SOCKS5 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 get_irc_text(line): return line[line[1:].find(':') + 2:] def get_irc_nick(source): full_nick = source[1:source.find('!')] return full_nick[:NICK_MAX_ENCODED+2] class ThrottleThread(threading.Thread): def __init__(self, irc): threading.Thread.__init__(self, name='ThrottleThread') self.daemon = True self.irc = irc self.msg_buffer = [] 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 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 shutdown(self): self.close() 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 Then, what is published is lines of form: !ordername !ordername .. fitting as many list entries as possible onto one line, up to the limit of the IRC parameters (see MAX_PRIVMSG_LEN). Order announce in private is handled by privmsg/_privmsg using chunking, no longer using this function. """ 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]] 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) 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 if len(message) > max_chunk_len: message_chunks = chunks(message, max_chunk_len) else: message_chunks = [message] for m in message_chunks: trailer = ' ~' if m == message_chunks[-1] else ' ;' if m == message_chunks[0]: m = COMMAND_PREFIX + cmd + ' ' + m self.send_raw(header + m + trailer, ob) 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("<