├── .gitignore ├── ReadMe.md ├── plugins ├── chatlog.py ├── pushbullet.py ├── webapp.py └── webapp │ ├── static │ └── buffer.html │ └── templates │ └── index.html ├── pushbullet.py ├── pushnotification.py ├── qt.py ├── quassel.py ├── quasselbot.py └── quasselclient.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /config.py 3 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # PyQuassel 2 | 3 | Pure python 3 implementation of QuasselClient. Doesn't depend on any PySide/PyQt libraries. 4 | 5 | Based on: 6 | * https://github.com/magne4000/node-libquassel/ 7 | * https://github.com/sandsmark/QuasselDroid/ 8 | 9 | ## Setup 10 | 11 | ### Install 12 | 13 | ```bash 14 | git clone https://github.com/Zren/PyQuassel.git 15 | cd PyQuassel 16 | ``` 17 | 18 | ### Create `config.py` 19 | 20 | ```python 21 | host = 'localhost' 22 | port = 4242 23 | username = 'AdminUser' 24 | password = 'PASSWORD' 25 | ``` 26 | 27 | ## Run 28 | 29 | ```bash 30 | python quasselbot.py 31 | ``` 32 | 33 | ## Update 34 | 35 | ```bash 36 | git pull origin master 37 | ``` 38 | 39 | # Plugins 40 | 41 | Enable a plugin by adding it's name in `config.py`. 42 | 43 | ```python 44 | enabledPlugins = [ 45 | 'chatlog', 46 | 'pushbullet', 47 | ] 48 | ``` 49 | 50 | ## chatlog 51 | 52 | Prints chat messages to the console. 53 | 54 | ## pushbullet 55 | 56 | Recieve push notifications for specific keywords or from query buffers. Current nick/highlight support not yet available. 57 | 58 | ![](https://i.imgur.com/G9lH5q8.png) ![](https://i.imgur.com/N9FjdB0.png) 59 | 60 | `config.py` 61 | 62 | ```python 63 | """ 64 | For push notifications, you'll need an Access Token which 65 | can gotten from your account settings. 66 | https://www.pushbullet.com/#settings/account 67 | """ 68 | pushbulletAccessToken = 'asd78f69sd876f765dsf78s5da7f5as7df8a58s7dfADS' 69 | """ 70 | To push to all decives, push set as None. 71 | To push to a specific device, enter the device name. 72 | https://www.pushbullet.com/#settings/devices 73 | """ 74 | pushbulletDeviceName = None 75 | # pushbulletDeviceName = 'Motorola XT1034' 76 | 77 | pushIfKeyword = [ # Case Insensitive 78 | 'Zren', 79 | 'Shadeness', 80 | 'Pushbullet', 81 | ] 82 | ``` 83 | 84 | ## webapp 85 | 86 | A very simple webserver to glance at recent messages. When enabled, the bot uses a total ~30 Mb of RAM. 87 | 88 | ![](https://i.imgur.com/49Kqeie.png) 89 | 90 | `config.py` 91 | 92 | ```python 93 | """ 94 | Webapp to read the last 50 messages in all channels. 95 | The pushbullet plugin will send links to the webapp if enabled. 96 | """ 97 | webappPort = 3000 98 | webappServerName = 'localhost' 99 | 100 | # The session key is used instead of username/password to view the webapp. 101 | # If left blank, a new key is generated each run. 102 | # Generate a good key with: python -c "import os; print(os.urandom(24))" 103 | # webappSessionKey = '' 104 | webappSessionKey = b'hN\xe7\xfd\x95[\xc0\xdfH\x96\xe4W\xaf\xad\xe2\x12#\xcfu\x92\x1eZ<\xf9' 105 | 106 | ``` 107 | -------------------------------------------------------------------------------- /plugins/chatlog.py: -------------------------------------------------------------------------------- 1 | from quassel import * 2 | import sys 3 | 4 | def onMessageReceived(bot, message): 5 | if message['type'] == Message.Type.Plain or message['type'] == Message.Type.Action: 6 | messageFormat = '[{}] {:<16}\t{:>16}: {}' 7 | output = messageFormat.format(*[ 8 | message['timestamp'].strftime('%H:%M'), 9 | message['bufferInfo']['name'], 10 | message['sender'].split('!')[0], 11 | message['content'], 12 | ]) 13 | # Strip stuff the console hates. 14 | output = output.encode(sys.stdout.encoding, errors='replace').decode(sys.stdout.encoding) 15 | print(output) 16 | -------------------------------------------------------------------------------- /plugins/pushbullet.py: -------------------------------------------------------------------------------- 1 | from quassel import * 2 | import re 3 | import base64 4 | 5 | pushNotification = None 6 | 7 | def onMessageReceived(bot, message): 8 | global pushNotification 9 | sendNotification = False 10 | 11 | if getattr(bot.config, 'pushbulletAccessToken', None) is None: 12 | return 13 | 14 | if message['type'] == Message.Type.Plain or message['type'] == Message.Type.Action: 15 | # Token match 16 | keywords = bot.config.pushIfKeyword 17 | pattern = r'\b(' + '|'.join(keywords) + r')\b' 18 | if re.search(pattern, message['content'], flags=re.IGNORECASE): 19 | sendNotification = True 20 | 21 | # Is Highlight 22 | # if message['flags'] & Message.Flag.Highlight: 23 | # sendNotification = True 24 | 25 | if message['bufferInfo']['type'] == BufferInfo.Type.QueryBuffer: 26 | sendNotification = True 27 | 28 | if message['flags'] & Message.Flag.Self: 29 | sendNotification = False 30 | 31 | if sendNotification: 32 | print(message) 33 | if pushNotification is None: 34 | from pushnotification import PushBulletNotification 35 | pushNotification = PushBulletNotification(bot.config.pushbulletAccessToken) 36 | if bot.config.pushbulletDeviceName: 37 | device = pushNotification.get_device(nickname=bot.config.pushbulletDeviceName) 38 | pushNotification.device = device 39 | 40 | data = {} 41 | if 'webapp' in bot.config.enabledPlugins: 42 | data['type'] = 'link' 43 | data['url'] = '{}#buffer-{}'.format(*[ 44 | bot.config.webappUrl, 45 | message['bufferInfo']['id'], 46 | ]) 47 | 48 | pushNotification.pushMessage(*[ 49 | message['bufferInfo']['name'], 50 | message['sender'].split('!')[0], 51 | message['content'], 52 | ], **data) 53 | -------------------------------------------------------------------------------- /plugins/webapp.py: -------------------------------------------------------------------------------- 1 | import re 2 | import base64 3 | from flask import Flask, render_template, request, redirect, session, jsonify 4 | from quassel import * 5 | from collections import defaultdict, deque 6 | 7 | app = Flask(__name__, static_folder='webapp/static', template_folder='webapp/templates') 8 | quasselClient = None 9 | bufferMessages = defaultdict(lambda: deque(maxlen=50)) 10 | 11 | def onSessionStarted(bot): 12 | global quasselClient 13 | quasselClient = bot 14 | 15 | import os 16 | 17 | if len(bot.config.webappSessionKey) > 0: 18 | if isinstance(bot.config.webappSessionKey, str): 19 | bot.config.webappSessionKey = bot.config.webappSessionKey.encode('utf-8') 20 | app.secret_key = bot.config.webappSessionKey # Bad? 21 | else: 22 | bot.config.webappSessionKey = os.urandom(24) 23 | app.secret_key = os.urandom(24) 24 | 25 | bot.config.webappUrl = 'http://{}:{}/?key={}'.format(*[ 26 | bot.config.webappServerName, 27 | bot.config.webappPort, 28 | base64.urlsafe_b64encode(bot.config.webappSessionKey).decode('utf-8'), 29 | ]) 30 | 31 | import threading 32 | thread = threading.Thread(target=app.run, kwargs={ 33 | 'host': '0.0.0.0', 34 | 'port': bot.config.webappPort, 35 | }) 36 | thread.daemon = True 37 | thread.start() 38 | 39 | print('[webapp] ' + bot.config.webappUrl) 40 | # import webbrowser 41 | # webbrowser.open(bot.config.webappUrl) 42 | 43 | def onMessageReceived(bot, message): 44 | if message['type'] in [Message.Type.Plain, Message.Type.Action]: 45 | messages = bufferMessages[message['bufferInfo']['id']] 46 | messages.append(Message(message)) 47 | 48 | import functools 49 | def require_login(f): 50 | @functools.wraps(f) 51 | def g(*args, **kwargs): 52 | key = request.values.get('key') 53 | if key: 54 | key = base64.urlsafe_b64decode(key.encode('utf-8')) 55 | if key == quasselClient.config.webappSessionKey: 56 | session['key'] = key 57 | 58 | if session.get('key') != quasselClient.config.webappSessionKey: 59 | return 'Not Logged In', 403 60 | 61 | ret = f(*args, **kwargs) 62 | return ret 63 | return g 64 | 65 | @app.route('/') 66 | @require_login 67 | def index(): 68 | def bufferSortKey(bufferId): 69 | b = quasselClient.buffers[bufferId] 70 | return (b['network'], b['name'].lower()) 71 | sortedBufferIds = list(bufferMessages.keys()) 72 | sortedBufferIds = sorted(sortedBufferIds, key=bufferSortKey) 73 | sortedBufferMessages = [(bufferId, bufferMessages[bufferId]) for bufferId in sortedBufferIds] 74 | return render_template('index.html', **{ 75 | 'bufferMessages': sortedBufferMessages, 76 | 'buffers': quasselClient.buffers, 77 | }) 78 | 79 | @app.route('/buffer//') 80 | @require_login 81 | def buffer(bufferId): 82 | return app.send_static_file('buffer.html') 83 | 84 | @app.route('/api/send', methods=['POST']) 85 | @require_login 86 | def api_send(): 87 | bufferId = int(request.values.get('bufferId')) 88 | message = request.values.get('message') 89 | quasselClient.sendInput(bufferId, message) 90 | return redirect('/') 91 | 92 | @app.route('/api/buffers/') 93 | @require_login 94 | def api_buffers(): 95 | return jsonify(buffers=quasselClient.buffers) 96 | 97 | @app.route('/api/buffers//') 98 | @require_login 99 | def api_buffer(bufferId): 100 | return jsonify(**quasselClient.buffers[bufferId]) 101 | 102 | @app.route('/api/buffers//messages/') 103 | @require_login 104 | def api_buffer_messages(bufferId): 105 | curBufferMessages = [{ 106 | 'id': message['id'], 107 | 'bufferId': message['bufferInfo']['id'], 108 | 'type': message['type'], 109 | 'flags': message['flags'], 110 | 'timestamp': message['timestamp'], 111 | 'sender': message['sender'], 112 | 'senderNick': message.senderNick, 113 | 'content': message['content'], 114 | } for message in bufferMessages[bufferId]] 115 | 116 | afterMessageId = int(request.args.get('afterMessageId', 0)) 117 | curBufferMessages = filter(lambda m: m['id'] > afterMessageId, curBufferMessages) 118 | curBufferMessages = list(curBufferMessages) 119 | 120 | return jsonify(messages=curBufferMessages) 121 | 122 | 123 | @app.errorhandler(Exception) 124 | def internal_error(error): 125 | import traceback 126 | tb = traceback.format_exc() 127 | import logging 128 | logging.error(tb) 129 | return "

500: Internal Error

{0}
".format(tb), 500 130 | 131 | -------------------------------------------------------------------------------- /plugins/webapp/static/buffer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PyQuassel WebApp 5 | 99 | 100 | 101 |
102 | 104 |
105 | 106 | 107 |
108 |
109 |
110 |
111 | 112 | 113 | 114 |
115 |
116 |
117 |
118 |
119 | 120 | 121 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /plugins/webapp/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | {% for bufferId, messages in bufferMessages %} 8 |

{{ buffers[bufferId]['name'] }}

9 | 10 | {% for message in messages %} 11 | 12 | 13 | 14 | 15 | 16 | {% endfor %} 17 |
{{ message.senderNick }}{{ message['content'] }}
18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 | {% endfor %} 26 |
27 | 28 | 62 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /pushbullet.py: -------------------------------------------------------------------------------- 1 | """ 2 | Uses 6Mb less RAM than the pushbullet python library. 3 | Using urllib instead of requests to shave off ~1Mb of RAM. 4 | """ 5 | 6 | import urllib.request 7 | import urllib.error 8 | import json 9 | 10 | class JsonSession: 11 | def __init__(self): 12 | self.headers = {} 13 | 14 | def request(self, url, data=None, headers=None, method='GET'): 15 | payload = json.dumps(data).encode('latin1') if data else None 16 | req_headers = self.headers 17 | if headers: 18 | req_headers = req_headers.copy().update(headers) 19 | 20 | req = urllib.request.Request(url, data=payload, headers=req_headers, method=method) 21 | with urllib.request.urlopen(req) as res: 22 | res_data = res.read().decode('utf-8') 23 | return json.loads(res_data) 24 | 25 | def get(self, *args, **kwargs): 26 | return self.request(*args, method='GET', **kwargs) 27 | 28 | def post(self, *args, **kwargs): 29 | return self.request(*args, method='POST', **kwargs) 30 | 31 | def delete(self, *args, **kwargs): 32 | return self.request(*args, method='DELETE', **kwargs) 33 | 34 | 35 | class PushBullet: 36 | def __init__(self, access_token): 37 | self.access_token = access_token 38 | self.session = JsonSession() 39 | self.session.headers = { 40 | 'Access-Token': self.access_token, 41 | 'Content-Type': 'application/json', 42 | } 43 | 44 | def get_device_list(self): 45 | # https://docs.pushbullet.com/#list-devices 46 | data = self.session.get('https://api.pushbullet.com/v2/devices') 47 | return data['devices'] 48 | 49 | def get_device(self, iden=None, nickname=None): 50 | device_list = self.get_device_list() 51 | for device in device_list: 52 | # pprint(device) 53 | if iden is not None and iden == device['iden']: 54 | return device 55 | if nickname is not None and nickname == device.get('nickname'): 56 | return device 57 | return None 58 | 59 | def get_push(self, push_iden): 60 | try: 61 | return self.session.get('https://api.pushbullet.com/v2/pushes/' + push_iden) 62 | except urllib.error.HTTPError: 63 | return None # Push was deleted 64 | 65 | def delete_push(self, push_iden): 66 | return self.session.delete('https://api.pushbullet.com/v2/pushes/' + push_iden) 67 | 68 | def push(self, **kwargs): 69 | # https://docs.pushbullet.com/#create-push 70 | return self.session.post('https://api.pushbullet.com/v2/pushes', data=kwargs) 71 | 72 | def push_note(self, title, body, **kwargs): 73 | data = {} 74 | data['type'] = 'note' 75 | data['title'] = title 76 | data['body'] = body 77 | data.update(kwargs) 78 | return self.push(**data) 79 | 80 | if __name__ == '__main__': 81 | import re 82 | from pprint import pprint 83 | 84 | import config 85 | p = PushBullet(config.pushbulletAccessToken) 86 | 87 | # device_list = p.get_device_list() 88 | # pprint(device_list) 89 | 90 | device = p.get_device(nickname=config.pushbulletDeviceName) 91 | device_iden = None if device is None else device['iden'] 92 | push = p.push_note('Meh', 'test', device_iden=device_iden) 93 | 94 | # push = p.get_push('ujAjoxHjkmisjAmJtb1z08') 95 | # pprint(push) 96 | -------------------------------------------------------------------------------- /pushnotification.py: -------------------------------------------------------------------------------- 1 | from pushbullet import PushBullet 2 | 3 | class PushBulletNotification(PushBullet): 4 | def __init__(self, apiKey): 5 | super().__init__(apiKey) 6 | self.activePush = None 7 | self.device = None 8 | 9 | @property 10 | def device_iden(self): 11 | return self.device['iden'] if self.device is not None else None 12 | 13 | def pushMessage(self, channel, senderNick, messageContent, **kwargs): 14 | messageFormat = '[{}] {}: {}' 15 | title = '' 16 | body = '' 17 | 18 | if self.activePush is not None: 19 | push = self.get_push(self.activePush['iden']) 20 | print(push) 21 | if not push['dismissed']: 22 | title = push.get('title', '') 23 | body = push.get('body', '') 24 | if push['active']: 25 | self.delete_push(self.activePush['iden']) 26 | 27 | messageLine = messageFormat.format(channel, senderNick, messageContent) 28 | if len(title) > 0: 29 | if len(body) > 0: 30 | body += '\n' + messageLine 31 | else: 32 | body = messageLine 33 | else: 34 | title = messageLine 35 | 36 | push = self.push_note(title=title, body=body, device_iden=self.device_iden, **kwargs) 37 | self.activePush = push 38 | 39 | if __name__ == '__main__': 40 | import logging 41 | logging.basicConfig(level=logging.DEBUG) 42 | 43 | import config 44 | # p = PushBullet(config.pushbulletApiKey) 45 | p = PushBulletNotification(config.pushbulletAccessToken) 46 | p.device = p.get_device(nickname=config.pushbulletDeviceName) 47 | # p.device = 'ujAjoxHjkmisjAcc26Tgia' 48 | 49 | 50 | p.pushMessage('#zren', 'Zren', 'Testing 1') 51 | import time 52 | time.sleep(3) 53 | p.pushMessage('#zren', 'Zren', 'Testing 2') 54 | time.sleep(10) 55 | p.pushMessage('#zren', 'Zren', 'Testing 3') 56 | 57 | 58 | -------------------------------------------------------------------------------- /qt.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | import socket 3 | import struct 4 | import datetime 5 | 6 | import json 7 | def pp(data): 8 | print(json.dumps(data, sort_keys=True, indent=4)) 9 | 10 | class QVariant: 11 | def __init__(self, obj): 12 | self.obj = obj 13 | if isinstance(obj, bool): 14 | self.type = QVariant.Type.BOOL 15 | elif isinstance(obj, int): 16 | self.type = QVariant.Type.UINT 17 | elif isinstance(obj, dict): 18 | self.type = QVariant.Type.MAP 19 | elif isinstance(obj, str): 20 | self.type = QVariant.Type.STRING 21 | elif isinstance(obj, list): 22 | self.type = QVariant.Type.LIST 23 | elif isinstance(obj, bytes): 24 | self.type = QVariant.Type.BYTEARRAY 25 | elif isinstance(obj, datetime.time): 26 | self.type = QVariant.Type.TIME 27 | 28 | class Type(IntEnum): 29 | # https://github.com/radekp/qt/blob/master/src/corelib/kernel/qvariant.h#L102 30 | # https://github.com/sandsmark/QuasselDroid/blob/8d8d7b34a515dfc7c570a5fa7392b877206b385b/QuasselDroid/src/main/java/com/iskrembilen/quasseldroid/protocol/qtcomm/QVariantType.java 31 | BOOL = 1 32 | INT = 2 33 | UINT = 3 34 | LONG = 4 35 | ULONG = 5 36 | CHAR = 7 37 | MAP = 8 38 | LIST = 9 39 | STRING = 10 40 | STRINGLIST = 11 41 | BYTEARRAY = 12 42 | TIME = 15 43 | DATETIME = 16 44 | USERTYPE = 127 45 | USHORT = 133 46 | 47 | class QUserType(QVariant, dict): 48 | def __init__(self, name, obj): 49 | super().__init__(obj) 50 | self.name = name 51 | self.type = QVariant.Type.USERTYPE 52 | 53 | def __repr__(self): 54 | return self.obj.__repr__() 55 | 56 | 57 | 58 | class QTcpSocket: 59 | def __init__(self): 60 | self.socket = socket.socket() 61 | self.socket.settimeout(2) 62 | self.logReadBuffer = False 63 | self.readBufferLog = [] 64 | 65 | def connectToHost(self, hostName, port): 66 | self.socket.connect((hostName, port)) 67 | 68 | def disconnectFromHost(self): 69 | self.socket.close() 70 | 71 | def read(self, maxSize): 72 | buf = self.socket.recv(maxSize) 73 | while len(buf) < maxSize: 74 | buf += self.socket.recv(maxSize - len(buf)) 75 | 76 | # print('QTcpSocket >>', buf) 77 | if self.logReadBuffer: 78 | self.readBufferLog.append(buf) 79 | return buf 80 | 81 | def write(self, data): 82 | # print('QTcpSocket <<', data) 83 | self.socket.sendall(data) 84 | 85 | 86 | 87 | 88 | class QDataStream: 89 | class ByteOrder(IntEnum): 90 | BigEndian, LittleEndian = range(2) 91 | class FloatingPointPrecision(IntEnum): 92 | SinglePrecision, DoublePrecision = range(2) 93 | class Status(IntEnum): 94 | Ok, ReadPastEnd, ReadCorruptData, WriteFailed = range(4) 95 | class Version(IntEnum): 96 | Qt_1_0 = 1 # Version 1 (Qt 1.x) 97 | Qt_2_0 = 2 # Version 2 (Qt 2.0) 98 | Qt_2_1 = 3 # Version 3 (Qt 2.1, 2.2, 2.3) 99 | Qt_3_0 = 4 # Version 4 (Qt 3.0) 100 | Qt_3_1 = 5 # Version 5 (Qt 3.1, 3.2) 101 | Qt_3_3 = 6 # Version 6 (Qt 3.3) 102 | Qt_4_0 = 7 # Version 7 (Qt 4.0, Qt 4.1) 103 | Qt_4_2 = 8 # Version 8 (Qt 4.2) 104 | Qt_4_3 = 9 # Version 9 (Qt 4.3) 105 | Qt_4_4 = 10 # Version 10 (Qt 4.4) 106 | Qt_4_5 = 11 # Version 11 (Qt 4.5) 107 | Qt_4_6 = 12 # Version 12 (Qt 4.6, Qt 4.7, Qt 4.8) 108 | 109 | def __init__(self, device=None): 110 | self.version = QDataStream.Version.Qt_4_6 111 | self.status = QDataStream.Status.Ok 112 | self.device = device 113 | 114 | def writeRawData(self, data): 115 | self.device.write(data) 116 | 117 | def writeUInt32BE(self, i): 118 | self << struct.pack('>I', i) 119 | 120 | def writeBool(self, b): 121 | self << struct.pack('?', b) 122 | 123 | def __lshift__(self, obj): 124 | self.writeRawData(obj) 125 | 126 | def readByte(self): 127 | buf = self.device.read(1) 128 | # print(buf) 129 | return buf 130 | # i = struct.unpack('>H', b'\x00' + buf)[0] 131 | # i = struct.unpack('b', buf)[0] 132 | # i = int.from_bytes(buf, byteorder='little', signed=True) 133 | # return i 134 | 135 | def readUInt8(self): 136 | buf = self.device.read(1) 137 | i = struct.unpack('B', buf)[0] 138 | return i 139 | 140 | def readInt16BE(self): 141 | buf = self.device.read(2) 142 | i = struct.unpack('>h', buf)[0] 143 | return i 144 | 145 | def readUInt16BE(self): 146 | buf = self.device.read(2) 147 | i = struct.unpack('>H', buf)[0] 148 | return i 149 | 150 | def readInt32BE(self): 151 | buf = self.device.read(4) 152 | i = struct.unpack('>i', buf)[0] 153 | return i 154 | 155 | def readUInt32BE(self): 156 | buf = self.device.read(4) 157 | i = struct.unpack('>I', buf)[0] 158 | return i 159 | 160 | def readBool(self): 161 | buf = self.device.read(1) 162 | b = struct.unpack('?', buf)[0] 163 | return b 164 | 165 | 166 | class Writer: 167 | def __init__(self, obj): 168 | self.buf = bytearray() 169 | self.type = QVariant(obj).type 170 | self.write(obj) 171 | 172 | @property 173 | def size(self): 174 | return len(self.buf) 175 | 176 | def __lshift__(self, obj): 177 | self.buf += obj 178 | 179 | def writeInt16BE(self, i): 180 | self << struct.pack('>h', i) 181 | 182 | def writeInt32BE(self, i): 183 | self << struct.pack('>i', i) 184 | 185 | def writeUInt32BE(self, i): 186 | self << struct.pack('>I', i) 187 | 188 | def writeBool(self, b): 189 | self << struct.pack('?', b) 190 | 191 | def write(self, obj): 192 | if obj is None: 193 | return None 194 | # AutoBoxed Types 195 | elif isinstance(obj, bool): 196 | self.writeQBool(obj) 197 | elif isinstance(obj, int): 198 | self.writeQUInt(obj) 199 | elif isinstance(obj, dict): 200 | self.writeQMap(obj) 201 | elif isinstance(obj, str): 202 | self.writeQString(obj) 203 | elif isinstance(obj, list): 204 | self.writeQList(obj) 205 | elif isinstance(obj, bytes): 206 | self.writeQByteArray(obj) 207 | elif isinstance(obj, datetime.time): 208 | self.writeQTime(obj) 209 | # Fuck 210 | else: 211 | raise Exception("Type not found") 212 | 213 | 214 | def writeQShort(self, qint): 215 | self.writeInt16BE(qint) 216 | 217 | def writeQInt(self, qint): 218 | self.writeInt32BE(qint) 219 | 220 | def writeQUInt(self, quint): 221 | self.writeUInt32BE(quint) 222 | 223 | def writeQBool(self, qbool): 224 | self.writeBool(qbool) 225 | 226 | def writeQTime(self, qtime): 227 | t = qtime.hour * 3600000 228 | t += qtime.minute * 60000 229 | t += qtime.second * 1000 230 | t += int(qtime.microsecond/1000) 231 | self.writeUInt32BE(t) 232 | 233 | def writeQDateTime(self, qdatetime): 234 | pass 235 | 236 | def writeQString(self, qstring): 237 | if qstring is None: 238 | # Special case for NULL 239 | self.writeUInt32BE(0xffffffff) 240 | else: 241 | # Converts to a UTF-16 buffer 242 | b = qstring.encode('utf_16_be') 243 | self.writeUInt32BE(len(b)) 244 | self << b 245 | 246 | def writeQByteArray(self, qbytearray): 247 | # Support passing str (for null check) 248 | if isinstance(qbytearray, str): 249 | # Converts to a UTF-8 buffer 250 | qbytearray = qbytearray.encode('utf-8') 251 | 252 | if qbytearray is None: 253 | # Special case for NULL 254 | self.writeUInt32BE(0xffffffff) 255 | else: 256 | self.writeUInt32BE(len(qbytearray)) 257 | self << qbytearray 258 | 259 | def writeQVariant(self, qvariant): 260 | # print(qvariant.type, qvariant.obj) 261 | self.writeUInt32BE(qvariant.type) 262 | self.writeBool(qvariant.obj is None) 263 | if qvariant.type == QVariant.Type.USERTYPE: 264 | self.writeQByteArray(qvariant.name) 265 | if qvariant.name == 'BufferInfo': 266 | self.writeQInt(qvariant.obj['id']) 267 | self.writeQInt(qvariant.obj['network']) 268 | self.writeQShort(qvariant.obj['type']) 269 | self.writeQInt(qvariant.obj['group']) 270 | self.writeQByteArray(qvariant.obj['name']) 271 | else: 272 | self.write(qvariant.obj) 273 | 274 | def writeQMap(self, m): 275 | size = len(m) 276 | self.writeUInt32BE(size) 277 | for key, value in m.items(): 278 | self.write(key) 279 | v = value 280 | if not isinstance(v, QVariant): 281 | v = QVariant(v) 282 | self.writeQVariant(v) 283 | 284 | def writeQList(self, l): 285 | size = len(l) 286 | self.writeUInt32BE(size) 287 | for item in l: 288 | v = item 289 | if not isinstance(v, QVariant): 290 | v = QVariant(v) 291 | self.writeQVariant(v) 292 | 293 | def write(self, obj): 294 | writer = QDataStream.Writer(obj) 295 | 296 | self.writeUInt32BE(writer.size + 5) 297 | self.writeUInt32BE(writer.type) 298 | self.writeBool(writer.size > 0) 299 | self << writer.buf 300 | 301 | # fmt = "" 302 | # values = [] 303 | 304 | # ftm += "" 305 | # bufs = [] 306 | # size = 0 307 | # type = QVariant.Types.Map 308 | # buf = 309 | 310 | def read(self): 311 | # size = self.readUInt32BE() 312 | buf = self.device.read(4) 313 | if len(buf) == 0: 314 | raise IOError('Device Closed') 315 | size = struct.unpack('>I', buf)[0] 316 | 317 | # print('buffer size:', size) 318 | obj = self.readQVariant() 319 | 320 | # pp(obj) 321 | # print(obj) 322 | 323 | return obj 324 | 325 | def readQVariant(self): 326 | variantType = self.readUInt32BE() 327 | try: 328 | variantType = QVariant.Type(variantType) 329 | except ValueError: 330 | 331 | m = self.readQUInt() 332 | print(m) 333 | raise Exception('QVariant.Type', variantType) 334 | 335 | # print(' QVariant.type', variantType) 336 | isNull = self.readBool() 337 | # print(' QVariant.isNull', isNull) 338 | 339 | if variantType == QVariant.Type.MAP: 340 | val = self.readQMap() 341 | elif variantType == QVariant.Type.BOOL: 342 | val = self.readQBool() 343 | elif variantType == QVariant.Type.STRING: 344 | val = self.readQString() 345 | elif variantType == QVariant.Type.CHAR: 346 | val = self.device.read(2) 347 | val = val.decode('utf_16_be') 348 | elif variantType == QVariant.Type.INT: 349 | val = self.readQInt() 350 | elif variantType == QVariant.Type.UINT: 351 | val = self.readQUInt() 352 | elif variantType == QVariant.Type.LIST: 353 | val = self.readQList() 354 | elif variantType == QVariant.Type.STRINGLIST: 355 | val = self.readQStringList() 356 | elif variantType == QVariant.Type.BYTEARRAY: 357 | val = self.readQByteArray() 358 | elif variantType == QVariant.Type.USHORT: 359 | val = self.readQUShort() 360 | elif variantType == QVariant.Type.TIME: 361 | val = self.readQTime() 362 | elif variantType == QVariant.Type.DATETIME: 363 | val = self.readQDateTime() 364 | elif variantType == QVariant.Type.USERTYPE: 365 | name = self.readQByteArray() 366 | name = name.decode('utf-8') 367 | name = name.rstrip('\0') 368 | # print('QUserType.name', name) 369 | 370 | val = self.readUserType(name) 371 | 372 | if val is None: 373 | raise Exception("QUserType.name", name) 374 | else: 375 | raise Exception('QVariant.type', variantType) 376 | 377 | # print('QVariant.val', variantType, val) 378 | return val 379 | 380 | def readUserType(self, name): 381 | return None 382 | 383 | def readQMap(self): 384 | size = self.readUInt32BE() 385 | # print('QMap.size', size) 386 | 387 | ret = {} 388 | for i in range(size): 389 | key = self.readQString() 390 | # print('QMap[key]', key) 391 | value = self.readQVariant() 392 | # print('QMap[value]', value) 393 | ret[key] = value 394 | return ret 395 | 396 | 397 | def readQString(self): 398 | size = self.readUInt32BE() 399 | if size == 0xFFFFFFFF: 400 | return '' 401 | buf = self.device.read(size) 402 | s = buf.decode('utf_16_be') 403 | return s 404 | 405 | def readQBool(self): 406 | return self.readBool() 407 | 408 | def readQInt(self): 409 | return self.readInt32BE() 410 | 411 | def readQUInt(self): 412 | return self.readUInt32BE() 413 | 414 | def readQShort(self): 415 | return self.readInt16BE() 416 | 417 | def readQUShort(self): 418 | return self.readUInt16BE() 419 | 420 | def readQTime(self): 421 | # https://github.com/sandsmark/QuasselDroid/blob/8d8d7b34a515dfc7c570a5fa7392b877206b385b/QuasselDroid/src/main/java/com/iskrembilen/quasseldroid/protocol/qtcomm/serializers/QTime.java 422 | millisSinceMidnight = self.readQUInt() 423 | hour = millisSinceMidnight // 3600000 424 | minute = (millisSinceMidnight - (hour * 3600000)) // 60000 425 | second = (millisSinceMidnight - (hour * 3600000) - (minute * 60000)) // 1000 426 | millis = millisSinceMidnight - (hour * 3600000) - (minute * 60000) - (second * 1000) 427 | t = datetime.time(hour, minute, second, microsecond=millis*1000) 428 | return t 429 | 430 | def readQDateTime(self): 431 | # https://github.com/magne4000/node-qtdatastream/blob/master/lib/reader.js#L146 432 | # https://github.com/sandsmark/QuasselDroid/blob/master/QuasselDroid/src/main/java/com/iskrembilen/quasseldroid/protocol/qtcomm/serializers/QDateTime.java 433 | # https://github.com/radekp/qt/blob/master/src/corelib/tools/qdatetime.cpp#L126 434 | julianDay = self.readQUInt() 435 | J = float(julianDay) + 0.5 436 | j = int(J + 32044) 437 | g = j // 146097; 438 | dg = j % 146097; 439 | c = (((dg // 36524) + 1) * 3) // 4 440 | dc = dg - c * 36524 441 | b = dc // 1461; 442 | db = dc % 1461 443 | a = (db // 365 + 1) * 3 // 4 444 | da = db - a * 365 445 | y = g * 400 + c * 100 + b * 4 + a 446 | m = (da * 5 + 308) // 153 - 2 447 | d = da - (m + 4) * 153 // 5 + 122 448 | 449 | year = int(y - 4800 + (m + 2) // 12) 450 | month = int((m + 2) % 12 + 1) 451 | day = int(d + 1) 452 | 453 | 454 | secondsSinceMidnight = self.readQUInt() 455 | hour = secondsSinceMidnight // 3600000 456 | minute = (secondsSinceMidnight - (hour * 3600000)) // 60000 457 | second = (secondsSinceMidnight - (hour * 3600000) - (minute * 60000)) // 1000 458 | 459 | t = datetime.datetime(year, month, day, hour, minute, second) 460 | 461 | isUTC = self.readBool() 462 | if isUTC: 463 | t = t.replace(tzinfo=datetime.timezone.utc) 464 | 465 | return t 466 | 467 | def readQList(self): 468 | size = self.readUInt32BE() 469 | l = [] 470 | for i in range(size): 471 | val = self.readQVariant() 472 | l.append(val) 473 | return l 474 | 475 | def readQStringList(self): 476 | size = self.readUInt32BE() 477 | l = [] 478 | for i in range(size): 479 | val = self.readQString() 480 | l.append(val) 481 | return l 482 | 483 | def readQByteArray(self): 484 | size = self.readUInt32BE() 485 | if size == 0xFFFFFFFF: 486 | return b'' 487 | buf = self.device.read(size) 488 | s = buf 489 | return s 490 | -------------------------------------------------------------------------------- /quassel.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | class Message(dict): 4 | class Type(IntEnum): 5 | Plain = 0x00001 6 | Notice = 0x00002 7 | Action = 0x00004 8 | Nick = 0x00008 9 | Mode = 0x00010 10 | Join = 0x00020 11 | Part = 0x00040 12 | Quit = 0x00080 13 | Kick = 0x00100 14 | Kill = 0x00200 15 | Server = 0x00400 16 | Info = 0x00800 17 | Error = 0x01000 18 | DayChange = 0x02000 19 | Topic = 0x04000 20 | NetsplitJoin = 0x08000 21 | NetsplitQuit = 0x10000 22 | Invite = 0x20000 23 | 24 | class Flag(IntEnum): 25 | NoFlags = 0x00 26 | Self = 0x01 27 | Highlight = 0x02 # Set clientside: https://github.com/quassel/quassel/blob/b49c64970b6237fc95f8ca88c8bb6bcf04c251d7/src/qtui/qtuimessageprocessor.cpp#L112 28 | Redirected = 0x04 29 | ServerMsg = 0x08 30 | Backlog = 0x80 31 | 32 | @property 33 | def senderNick(self): 34 | return self['sender'].split('!')[0] 35 | 36 | 37 | class BufferInfo: 38 | class Type(IntEnum): 39 | InvalidBuffer = 0x00 40 | StatusBuffer = 0x01 41 | ChannelBuffer = 0x02 42 | QueryBuffer = 0x04 43 | GroupBuffer = 0x08 44 | 45 | class Activity(IntEnum): 46 | NoActivity = 0x00 47 | OtherActivity = 0x01 48 | NewMessage = 0x02 49 | Highlight = 0x40 50 | 51 | 52 | class RequestType(IntEnum): 53 | Invalid = 0 54 | Sync = 1 55 | RpcCall = 2 56 | InitRequest = 3 57 | InitData = 4 58 | HeartBeat = 5 59 | HeartBeatReply = 6 60 | 61 | class Protocol: 62 | magic = 0x42b33f00 63 | 64 | class Type: 65 | InternalProtocol = 0x00 66 | LegacyProtocol = 0x01 67 | DataStreamProtocol = 0x02 68 | 69 | class Feature: 70 | Encryption = 0x01 71 | Compression = 0x02 72 | -------------------------------------------------------------------------------- /quasselbot.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os.path 3 | from importlib.machinery import SourceFileLoader 4 | import traceback 5 | from quassel import * 6 | from quasselclient import QuasselClient 7 | 8 | class QuasselBot(QuasselClient): 9 | def __init__(self, config): 10 | super().__init__(config) 11 | self.pushNotification = None 12 | self.plugins = [] 13 | self.defaultPlugins = [ 14 | 'logger', 15 | ] 16 | 17 | @property 18 | def pluginsToLoad(self): 19 | return getattr(self.config, 'enabledPlugins', self.defaultPlugins) 20 | 21 | def loadPlugins(self): 22 | for pluginFilepath in glob.glob('plugins/*.py'): 23 | self.loadPlugin(pluginFilepath) 24 | 25 | def loadPlugin(self, pluginFilepath): 26 | pluginName = pluginFilepath[len('plugins/'):-len('.py')] 27 | if pluginName not in self.pluginsToLoad: 28 | return 29 | 30 | try: 31 | pluginModuleName = 'plugin_' + pluginName 32 | loader = SourceFileLoader(pluginModuleName, pluginFilepath) 33 | pluginModule = loader.load_module() 34 | self.plugins.append(pluginModule) 35 | print('Plugin "{}" loaded.'.format(pluginFilepath)) 36 | except Exception as e: 37 | print('Error loading plugin "{}".'.format(pluginFilepath)) 38 | traceback.print_exc() 39 | 40 | def pluginCall(self, fnName, *args): 41 | for plugin in self.plugins: 42 | try: 43 | fn = getattr(plugin, fnName, None) 44 | if fn: 45 | fn(self, *args) 46 | except Exception as e: 47 | print('Error while calling {}.{}'.format(plugin.__name__, fnName)) 48 | traceback.print_exc() 49 | 50 | def onSessionStarted(self): 51 | # self.sendNetworkInits() # Slooooow. Also adds 4-8 Mb to RAM. 52 | # self.sendBufferInits() 53 | 54 | self.pluginCall('onSessionStarted') 55 | 56 | def onMessageReceived(self, message): 57 | self.pluginCall('onMessageReceived', message) 58 | 59 | def onSocketClosed(self): 60 | print('\n\nSocket Closed\n\nReconnecting\n') 61 | self.reconnect() 62 | 63 | 64 | 65 | if __name__ == '__main__': 66 | import sys, os 67 | if not os.path.exists('config.py'): 68 | print('Please create a config.py as mentioned in the ReadMe.') 69 | sys.exit(1) 70 | 71 | import config 72 | quasselClient = QuasselBot(config) 73 | quasselClient.loadPlugins() 74 | quasselClient.run() 75 | 76 | quasselClient.disconnectFromHost() 77 | -------------------------------------------------------------------------------- /quasselclient.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | import datetime 4 | from pprint import pprint 5 | 6 | from qt import * 7 | from quassel import * 8 | 9 | class QuasselQDataStream(QDataStream): 10 | def readUserType(self, name): 11 | if name == 'NetworkId': 12 | return self.readQInt() 13 | elif name == 'IdentityId': 14 | return self.readQInt() 15 | elif name == 'BufferId': 16 | return self.readQInt() 17 | elif name == 'MsgId': 18 | return self.readQInt() 19 | elif name == 'Identity': 20 | return self.readQMap() 21 | elif name == 'Network::Server': 22 | return self.readQMap() 23 | # print(val) 24 | elif name == 'BufferInfo': 25 | val = {} 26 | val['id'] = self.readQInt() 27 | val['network'] = self.readQInt() 28 | val['type'] = BufferInfo.Type(self.readQShort()) 29 | val['group'] = self.readQInt() 30 | val['name'] = self.readQByteArray().decode('utf-8') 31 | return val 32 | elif name == 'Message': 33 | val = {} 34 | val['id'] = self.readQInt() 35 | val['timestamp'] = datetime.datetime.fromtimestamp(self.readQUInt()) 36 | val['type'] = Message.Type(self.readQUInt()) 37 | val['flags'] = Message.Flag(self.readUInt8()) 38 | val['bufferInfo'] = self.readUserType('BufferInfo') 39 | val['sender'] = self.readQByteArray().decode('utf-8') 40 | val['content'] = self.readQByteArray().decode('utf-8') 41 | return val 42 | else: 43 | return None 44 | 45 | class QuasselClient: 46 | def __init__(self, config=None): 47 | self.clientName = 'QuasselClient.py' 48 | self.clientVersion = '2' 49 | self.clientDate = 'Jun 25 2018 21:00:00' 50 | 51 | self.config = config 52 | self.running = False 53 | self.socket = None 54 | self.stream = None 55 | 56 | def createSocket(self): 57 | self.socket = QTcpSocket() 58 | self.stream = QuasselQDataStream(self.socket) 59 | 60 | def connectToHost(self, hostName=None, port=None): 61 | if hostName is None: 62 | hostName = self.config.host 63 | if port is None: 64 | port = self.config.port 65 | 66 | if self.socket is None: 67 | self.createSocket() 68 | self.socket.connectToHost(hostName, port) 69 | 70 | def disconnectFromHost(self): 71 | self.socket.disconnectFromHost() 72 | 73 | def onSocketConnect(self): 74 | # https://github.com/quassel/quassel/blob/b49c64970b6237fc95f8ca88c8bb6bcf04c251d7/src/core/coreauthhandler.cpp#L57 75 | # https://github.com/sandsmark/QuasselDroid/blob/8d8d7b34a515dfc7c570a5fa7392b877206b385b/QuasselDroid/src/main/java/com/iskrembilen/quasseldroid/io/CoreConnection.java#L475 76 | self.stream.writeUInt32BE(Protocol.magic) 77 | self.stream.writeUInt32BE(Protocol.Type.LegacyProtocol) 78 | self.stream.writeUInt32BE(0x01 << 31) # protoFeatures 79 | 80 | data = self.stream.readUInt32BE() 81 | # print(data) 82 | connectionFeatures = data >> 24 83 | if (connectionFeatures & 0x01) > 0: 84 | print('Core Supports SSL') 85 | if (connectionFeatures & 0x02) > 0: 86 | print('Core Supports Compression') 87 | 88 | def sendClientInit(self): 89 | m = {} 90 | m['MsgType'] = 'ClientInit' 91 | m['ClientVersion'] = '{} v{}'.format(self.clientName, self.clientVersion) 92 | m['ClientDate'] = self.clientDate 93 | m['ProtocolVersion'] = 10 94 | m['UseCompression'] = False 95 | m['UseSsl'] = False 96 | self.stream.write(m) 97 | 98 | def readClientInit(self): 99 | data = self.stream.read() 100 | return data 101 | 102 | def sendClientLogin(self, username=None, password=None): 103 | if username is None: 104 | username = self.config.username 105 | if password is None: 106 | password = self.config.password 107 | m = {} 108 | m['MsgType'] = 'ClientLogin' 109 | m['User'] = username 110 | m['Password'] = password 111 | self.stream.write(m) 112 | 113 | def readClientLogin(self): 114 | data = self.stream.read() 115 | return data 116 | 117 | def readSessionState(self): 118 | data = self.stream.read() 119 | sessionState = data['SessionState'] 120 | self.buffers = {} 121 | for bufferInfo in sessionState['BufferInfos']: 122 | self.buffers[bufferInfo['id']] = bufferInfo 123 | 124 | self.networks = {} 125 | for networkId in sessionState['NetworkIds']: 126 | # print(networkId) 127 | self.networks[networkId] = None 128 | # print(self.networks) 129 | 130 | return data 131 | 132 | def sendNetworkInits(self): 133 | for networkId in self.networks.keys(): 134 | # print(networkId) 135 | l = [ 136 | RequestType.InitRequest, 137 | 'Network', 138 | str(networkId), 139 | ] 140 | self.stream.write(l) 141 | 142 | def sendBufferInits(self): 143 | for bufferId, buffer in self.buffers.items(): 144 | # print(networkId) 145 | l = [ 146 | RequestType.InitRequest, 147 | 'IrcChannel', 148 | '{}/{}'.format(buffer['network'], buffer['name']), 149 | ] 150 | self.stream.write(l) 151 | 152 | 153 | def readPackedFunc(self): 154 | data = self.stream.read() 155 | requestType = data[0] 156 | if requestType == RequestType.RpcCall: 157 | functionName = data[1] 158 | if functionName == b'2displayMsg(Message)': 159 | message = data[2] 160 | # print(message) 161 | self.onMessageReceived(message) 162 | return 163 | 164 | elif requestType == RequestType.InitData: 165 | className = data[1] 166 | objectName = data[2] 167 | if className == b'Network': 168 | networkId = int(objectName) 169 | initMap = data[3] 170 | # pprint(initMap) 171 | del data 172 | del initMap['IrcUsersAndChannels'] 173 | 174 | networkInfo = {} 175 | networkInfo['networkName'] = initMap['networkName'] 176 | self.networks[networkId] = networkInfo 177 | # print(initMap['networkName']) 178 | # if False: 179 | # if initMap['networkName'] == 'Freenode': 180 | # for key in initMap.keys(): 181 | # print('\tinitMap[{}]'.format(key)) 182 | # # del initMap['IrcUsersAndChannels'] 183 | 184 | # with open('output-network.log', 'w') as f: 185 | # f.write(str(initMap).replace(', \'', ',\n\'')) 186 | # # pprint(initMap) 187 | return 188 | elif className == b'IrcChannel': 189 | networkId, bufferName = objectName.split('/') 190 | networkId = int(networkId) 191 | for buffer in self.buffers.values(): 192 | if buffer['network'] == networkId and buffer['name'] == bufferName: 193 | initMap = data[3] 194 | del data 195 | del initMap['UserModes'] 196 | buffer['isJoined'] = True 197 | buffer['topic'] = initMap['topic'] 198 | break 199 | return 200 | elif requestType == RequestType.HeartBeat: 201 | self.sendHeartBeatReply() 202 | return 203 | elif requestType == RequestType.HeartBeatReply: 204 | # print('HeartBeatReply', data) 205 | return 206 | 207 | # import sys 208 | # output = str(data) 209 | # output = output.encode(sys.stdout.encoding, errors='replace').decode(sys.stdout.encoding) 210 | # print(output) 211 | 212 | def sendInput(self, bufferId, message): 213 | print('sendInput', bufferId, message) 214 | bufferInfo = self.buffers[bufferId] 215 | l = [ 216 | RequestType.RpcCall, 217 | '2sendInput(BufferInfo,QString)', 218 | QUserType('BufferInfo', bufferInfo), 219 | message, 220 | ] 221 | pprint(l) 222 | self.stream.write(l) 223 | 224 | def sendHeartBeat(self): 225 | t = datetime.datetime.now().time() 226 | # print('sendHeartBeat', t) 227 | l = [ 228 | RequestType.HeartBeat, 229 | t, 230 | ] 231 | self.stream.write(l) 232 | 233 | def sendHeartBeatReply(self): 234 | t = datetime.datetime.now().time() 235 | # print('sendHeartBeatReply', t) 236 | l = [ 237 | RequestType.HeartBeatReply, 238 | t, 239 | ] 240 | self.stream.write(l) 241 | 242 | # findBufferId(..., networkName="") requires calling quasselClient.sendNetworkInits() first. 243 | def findBufferId(self, bufferName, networkId=None, networkName=None): 244 | for buffer in self.buffers.values(): 245 | if buffer['name'] == bufferName: 246 | if networkId is not None: 247 | if buffer['network'] == networkId: 248 | return buffer['id'] 249 | elif networkName is not None: 250 | network = self.networks[buffer['network']] 251 | if network['networkName'] == networkName: 252 | return buffer['id'] 253 | else: 254 | return buffer['id'] 255 | return None 256 | 257 | def createSession(self): 258 | self.connectToHost() 259 | self.onSocketConnect() 260 | 261 | self.sendClientInit() 262 | self.readClientInit() 263 | self.sendClientLogin() 264 | self.readClientLogin() 265 | 266 | self.readSessionState() 267 | 268 | def reconnect(self): 269 | self.createSession() 270 | self.running = True 271 | 272 | def run(self): 273 | self.createSession() 274 | self.onSessionStarted() 275 | self.running = True 276 | self.lastHeartBeatSentAt = None 277 | while self.running: 278 | try: 279 | self.readPackedFunctionLoop() 280 | except IOError: 281 | self.running = False 282 | self.onSocketClosed() 283 | 284 | 285 | def readPackedFunctionLoop(self): 286 | self.socket.socket.settimeout(15) 287 | self.socket.logReadBuffer = False 288 | while self.running: 289 | try: 290 | self.readPackedFunc() 291 | # print('TCP >>') 292 | # for buf in self.socket.readBufferLog: 293 | # print('\t', buf) 294 | del self.socket.readBufferLog[:] 295 | 296 | t = int(time.time() * 1000) 297 | if self.lastHeartBeatSentAt is None or t - self.lastHeartBeatSentAt > 60 * 1000: 298 | self.sendHeartBeat() 299 | self.lastHeartBeatSentAt = t 300 | 301 | # import gc 302 | # gc.collect() 303 | except socket.timeout: 304 | pass 305 | except Exception as e: 306 | import traceback 307 | traceback.print_exc() 308 | print('TCP >>') 309 | for buf in self.socket.readBufferLog: 310 | print('\t', buf) 311 | raise e 312 | 313 | def onSessionStarted(self): 314 | self.sendNetworkInits() # Slooooow. 315 | self.sendBufferInits() 316 | 317 | def onMessageReceived(self, message): 318 | pass 319 | 320 | def onSocketClosed(self): 321 | pass 322 | 323 | if __name__ == '__main__': 324 | raise Exception("You ran the wrong file.") 325 | 326 | import sys, os 327 | if not os.path.exists('config.py'): 328 | print('Please create a config.py as mentioned in the ReadMe.') 329 | sys.exit(1) 330 | 331 | import config 332 | quasselClient = QuasselClient(config) 333 | quasselClient.run() 334 | 335 | quasselClient.disconnectFromHost() 336 | --------------------------------------------------------------------------------