1# COPYRIGHT (C) 2020-2021 Nicotine+ Team
2# COPYRIGHT (C) 2009-2011 Quinox <quinox@users.sf.net>
3# COPYRIGHT (C) 2007-2009 Daelstorm <daelstorm@gmail.com>
4# COPYRIGHT (C) 2003-2004 Hyriand <hyriand@thegraveyard.org>
5# COPYRIGHT (C) 2001-2003 Alexander Kanavin
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20import socket
21import struct
22import zlib
23
24from pynicotine.config import config
25from pynicotine.logfacility import log
26from pynicotine.utils import debug
27
28""" This module contains message classes, that networking and UI thread
29exchange. Basically there are three types of messages: internal messages,
30server messages and p2p messages (between clients). """
31
32
33INT_SIZE = struct.calcsize("<i")
34INT64_SIZE = struct.calcsize("<q")
35
36INT_UNPACK = struct.Struct("<i").unpack
37UINT_UNPACK = struct.Struct("<I").unpack
38UINT64_UNPACK = struct.Struct("<Q").unpack
39
40INT_PACK = struct.Struct("<i").pack
41UINT_PACK = struct.Struct("<I").pack
42UINT64_PACK = struct.Struct("<Q").pack
43
44SEARCH_TOKENS_ALLOWED = set()
45
46
47class InternalMessage:
48    pass
49
50
51class Conn(InternalMessage):
52
53    __slots__ = ("conn", "addr", "init")
54
55    def __init__(self, conn=None, addr=None, init=None):
56        self.conn = conn
57        self.addr = addr
58        self.init = init
59
60
61class InitServerConn(Conn):
62    """ NicotineCore sends this to make networking thread establish a server connection.
63    When a connection is established, networking thread returns an object of this type
64    to NicotineCore. """
65
66
67class InitPeerConn(Conn):
68    """ NicotineCore sends this to make networking thread establish a peer connection.
69    When a connection is established, networking thread returns an object of this type
70    to NicotineCore. """
71
72
73class IncConn(Conn):
74    """ Sent by networking thread to indicate an incoming connection."""
75
76
77class ConnClose(InternalMessage):
78    """ Sent by networking thread to indicate a connection has been closed."""
79
80    __slots__ = ("conn", "addr", "callback")
81
82    def __init__(self, conn=None, addr=None, callback=True):
83        self.conn = conn
84        self.addr = addr
85        self.callback = callback
86
87
88class ConnCloseIP(InternalMessage):
89    """ Sent by the main thread to the networking thread in order to close any connections
90    using a certain IP address. """
91
92    def __init__(self, addr=None):
93        self.addr = addr
94
95
96class ConnectError(InternalMessage):
97    """ Sent when a socket exception occurs. It's up to UI thread to
98    handle this."""
99
100    __slots__ = ("connobj", "err")
101
102    def __init__(self, connobj=None, err=None):
103        self.connobj = connobj
104        self.err = err
105
106
107class ConnectToPeerTimeout:
108
109    __slots__ = ("conn",)
110
111    def __init__(self, conn):
112        self.conn = conn
113
114
115class MessageProgress(InternalMessage):
116    """ Used to indicate progress of long transfers. """
117
118    __slots__ = ("user", "msg_type", "position", "total")
119
120    def __init__(self, user=None, msg_type=None, position=None, total=None):
121        self.user = user
122        self.msg_type = msg_type
123        self.position = position
124        self.total = total
125
126
127class TransferTimeout:
128
129    __slots__ = ("transfer",)
130
131    def __init__(self, transfer):
132        self.transfer = transfer
133
134
135class CheckDownloadQueue(InternalMessage):
136    """ Sent from a timer to the main thread to indicate that stuck downloads
137    should be checked. """
138
139
140class CheckUploadQueue(InternalMessage):
141    """ Sent from a timer to the main thread to indicate that the upload queue
142    should be checked. """
143
144
145class DownloadFile(InternalMessage):
146    """ Sent by networking thread to indicate file transfer progress.
147    Sent by UI to pass the file object to write. """
148
149    __slots__ = ("conn", "file")
150
151    def __init__(self, conn=None, file=None):
152        self.conn = conn
153        self.file = file
154
155
156class UploadFile(InternalMessage):
157
158    __slots__ = ("conn", "file", "size", "sentbytes", "offset")
159
160    def __init__(self, conn=None, file=None, size=None, sentbytes=0, offset=None):
161        self.conn = conn
162        self.file = file
163        self.size = size
164        self.sentbytes = sentbytes
165        self.offset = offset
166
167
168class FileError(InternalMessage):
169    """ Sent by networking thread to indicate that a file error occurred during
170    filetransfer. """
171
172    __slots__ = ("conn", "file", "strerror")
173
174    def __init__(self, conn=None, file=None, strerror=None):
175        self.conn = conn
176        self.file = file
177        self.strerror = strerror
178
179
180class SetUploadLimit(InternalMessage):
181    """ Sent by the GUI thread to indicate changes in bandwidth shaping rules"""
182
183    def __init__(self, uselimit, limit, limitby):
184        self.uselimit = uselimit
185        self.limit = limit
186        self.limitby = limitby
187
188
189class SetDownloadLimit(InternalMessage):
190    """ Sent by the GUI thread to indicate changes in bandwidth shaping rules"""
191
192    def __init__(self, limit):
193        self.limit = limit
194
195
196class SetCurrentConnectionCount(InternalMessage):
197    """ Sent by networking thread to update the number of current
198    connections shown in the GUI. """
199
200    __slots__ = ("msg",)
201
202    def __init__(self, msg):
203        self.msg = msg
204
205
206class SlskMessage:
207    """ This is a parent class for all protocol messages. """
208
209    def get_object(self, message, obj_type, start=0, getsignedint=False, getunsignedlonglong=False):
210        """ Returns object of specified type, extracted from message (which is
211        a binary array). start is an offset."""
212
213        try:
214            if obj_type is int:
215                if getsignedint:
216                    # little-endian signed integer (4 bytes)
217                    return INT_SIZE + start, INT_UNPACK(message[start:start + INT_SIZE])[0]
218
219                if getunsignedlonglong:
220                    # little-endian unsigned long long (8 bytes)
221                    try:
222                        return INT64_SIZE + start, UINT64_UNPACK(message[start:start + INT64_SIZE])[0]
223
224                    except Exception:
225                        # Fall back to unsigned integer
226                        pass
227
228                # little-endian unsigned integer (4 bytes)
229                return INT_SIZE + start, UINT_UNPACK(message[start:start + INT_SIZE])[0]
230
231            if obj_type is bytes:
232                length = UINT_UNPACK(message[start:start + INT_SIZE])[0]
233                content = message[start + INT_SIZE:start + length + INT_SIZE]
234
235                return length + INT_SIZE + start, content
236
237            if obj_type is str:
238                length = UINT_UNPACK(message[start:start + INT_SIZE])[0]
239                string = message[start + INT_SIZE:start + length + INT_SIZE]
240
241                try:
242                    string = str(string, "utf-8")
243                except Exception:
244                    # Older clients (Soulseek NS)
245
246                    try:
247                        string = str(string, "latin-1")
248                    except Exception as error:
249                        log.add("Error trying to decode string '%s': %s", (string, error))
250
251                return length + INT_SIZE + start, string
252
253            return start, None
254
255        except struct.error as error:
256            log.add("%s %s trying to unpack %s at '%s' at %s/%s",
257                    (self.__class__, error, obj_type, bytes(message[start:]), start, len(message)))
258            raise struct.error(error)
259
260    def pack_object(self, obj, signedint=False, unsignedlonglong=False, latin1=False):
261        """ Returns object (integer, long or string packed into a
262        binary array."""
263
264        if isinstance(obj, int):
265            if signedint:
266                return INT_PACK(obj)
267
268            if unsignedlonglong:
269                return UINT64_PACK(obj)
270
271            return UINT_PACK(obj)
272
273        if isinstance(obj, bytes):
274            return UINT_PACK(len(obj)) + obj
275
276        if isinstance(obj, str):
277            if latin1:
278                try:
279                    # Try to encode in latin-1 first for older clients (Soulseek NS)
280                    encoded = bytes(obj, "latin-1")
281                except Exception:
282                    encoded = bytes(obj, "utf-8", "replace")
283            else:
284                encoded = bytes(obj, "utf-8", "replace")
285
286            return UINT_PACK(len(encoded)) + encoded
287
288        log.add("Warning: unknown object type %(obj_type)s in message %(msg_type)s",
289                {'obj_type': type(obj), 'msg_type': self.__class__})
290
291        return b""
292
293    def make_network_message(self):
294        """ Returns binary array, that can be sent over the network"""
295
296        log.add("Empty message made, class %s", self.__class__)
297        return b""
298
299    def parse_network_message(self, _message):
300        """ Extracts information from the message and sets up fields
301        in an object"""
302
303        log.add("Can't parse incoming messages, class %s", self.__class__)
304
305    def debug(self, message=None):
306        debug(type(self).__name__, self.__dict__, message.__repr__())
307
308
309"""
310Server Messages
311"""
312
313
314class ServerMessage(SlskMessage):
315    """ This is a parent class for all server messages. """
316
317
318class Login(ServerMessage):
319    """ Server code: 1 """
320    """ We sent this to the server right after the connection has been
321    established. Server responds with the greeting message. """
322
323    def __init__(self, username=None, passwd=None, version=None, minorversion=None):
324        self.username = username
325        self.passwd = passwd
326        self.version = version
327        self.minorversion = minorversion
328        self.success = None
329        self.reason = None
330        self.banner = None
331        self.ip_address = None
332        self.checksum = None
333
334    def make_network_message(self):
335        from hashlib import md5
336
337        msg = bytearray()
338        msg.extend(self.pack_object(self.username))
339        msg.extend(self.pack_object(self.passwd))
340        msg.extend(self.pack_object(self.version))
341
342        payload = self.username + self.passwd
343        md5hash = md5(payload.encode()).hexdigest()
344        msg.extend(self.pack_object(md5hash))
345
346        msg.extend(self.pack_object(self.minorversion))
347
348        return msg
349
350    def parse_network_message(self, message):
351        pos, self.success = 1, message[0]
352
353        if not self.success:
354            pos, self.reason = self.get_object(message, str, pos)
355        else:
356            pos, self.banner = self.get_object(message, str, pos)
357
358        if not message[pos:]:
359            return
360
361        try:
362            pos, self.ip_address = pos + 4, socket.inet_ntoa(bytes(message[pos:pos + 4][::-1]))
363
364        except Exception as error:
365            log.add("Error unpacking IP address: %s", error)
366
367        # MD5 hexdigest of the password you sent
368        if message[pos:]:
369            pos, self.checksum = self.get_object(message, str, pos)
370
371
372class SetWaitPort(ServerMessage):
373    """ Server code: 2 """
374    """ We send this to the server to indicate the port number that we
375    listen on (2234 by default). """
376
377    def __init__(self, port=None):
378        self.port = port
379
380    def make_network_message(self):
381        return self.pack_object(self.port)
382
383
384class GetPeerAddress(ServerMessage):
385    """ Server code: 3 """
386    """ We send this to the server to ask for a peer's address
387    (IP address and port), given the peer's username. """
388
389    def __init__(self, user=None):
390        self.user = user
391        self.ip_address = None
392        self.port = None
393
394    def make_network_message(self):
395        return self.pack_object(self.user)
396
397    def parse_network_message(self, message):
398        pos, self.user = self.get_object(message, str)
399        pos, self.ip_address = pos + 4, socket.inet_ntoa(bytes(message[pos:pos + 4][::-1]))
400        pos, self.port = self.get_object(message, int, pos, 1)
401
402
403class AddUser(ServerMessage):
404    """ Server code: 5 """
405    """ Used to be kept updated about a user's stats. When a user's
406    stats have changed, the server sends a GetUserStats response message
407    with the new user stats. """
408
409    def __init__(self, user=None):
410        self.user = user
411        self.userexists = None
412        self.status = None
413        self.avgspeed = None
414        self.uploadnum = None
415        self.files = None
416        self.dirs = None
417        self.country = None
418
419    def make_network_message(self):
420        return self.pack_object(self.user)
421
422    def parse_network_message(self, message):
423        pos, self.user = self.get_object(message, str)
424        pos, self.userexists = pos + 1, message[pos]
425
426        if not message[pos:]:
427            # User does not exist
428            return
429
430        pos, self.status = self.get_object(message, int, pos)
431        pos, self.avgspeed = self.get_object(message, int, pos)
432        pos, self.uploadnum = self.get_object(message, int, pos, getunsignedlonglong=True)
433
434        pos, self.files = self.get_object(message, int, pos)
435        pos, self.dirs = self.get_object(message, int, pos)
436
437        if not message[pos:]:
438            # User is offline
439            return
440
441        pos, self.country = self.get_object(message, str, pos)
442
443
444class RemoveUser(ServerMessage):
445    """ Server code: 6 """
446    """ Used when we no longer want to be kept updated about a
447    user's stats. """
448
449    def __init__(self, user=None):
450        self.user = user
451
452    def make_network_message(self):
453        return self.pack_object(self.user)
454
455
456class GetUserStatus(ServerMessage):
457    """ Server code: 7 """
458    """ The server tells us if a user has gone away or has returned. """
459
460    def __init__(self, user=None):
461        self.user = user
462        self.status = None
463        self.privileged = None
464
465    def make_network_message(self):
466        return self.pack_object(self.user)
467
468    def parse_network_message(self, message):
469        pos, self.user = self.get_object(message, str)
470        pos, self.status = self.get_object(message, int, pos)
471
472        # Soulfind support
473        if message[pos:]:
474            pos, self.privileged = pos + 1, message[pos]
475
476
477class SayChatroom(ServerMessage):
478    """ Server code: 13 """
479    """ Either we want to say something in the chatroom, or someone else did. """
480
481    def __init__(self, room=None, msg=None):
482        self.room = room
483        self.msg = msg
484        self.user = None
485
486    def make_network_message(self):
487        return self.pack_object(self.room) + self.pack_object(self.msg)
488
489    def parse_network_message(self, message):
490        pos, self.room = self.get_object(message, str)
491        pos, self.user = self.get_object(message, str, pos)
492        pos, self.msg = self.get_object(message, str, pos)
493
494
495class UserData:
496    """ When we join a room, the server sends us a bunch of these for each user. """
497
498    __slots__ = ("username", "status", "avgspeed", "uploadnum", "files", "dirs", "slotsfull", "country")
499
500    def __init__(self, username=None, status=None, avgspeed=None, uploadnum=None, files=None, dirs=None,
501                 slotsfull=None, country=None):
502        self.username = username
503        self.status = status
504        self.avgspeed = avgspeed
505        self.uploadnum = uploadnum
506        self.files = files
507        self.dirs = dirs
508        self.slotsfull = slotsfull
509        self.country = country
510
511
512class JoinRoom(ServerMessage):
513    """ Server code: 14 """
514    """ We send this message to the server when we want to join a room. If the
515    room doesn't exist, it is created.
516
517    Server responds with this message when we join a room. Contains users list
518    with data on everyone. """
519
520    def __init__(self, room=None, private=None):
521        self.room = room
522        self.private = private
523        self.owner = None
524        self.users = []
525        self.operators = []
526
527    def make_network_message(self):
528        if self.private is not None:
529            return self.pack_object(self.room) + self.pack_object(self.private)
530
531        return self.pack_object(self.room)
532
533    def parse_network_message(self, message):
534        pos, self.room = self.get_object(message, str)
535        pos1 = pos
536        pos, self.users = self.get_users(message[pos:])
537        pos = pos1 + pos
538
539        if message[pos:]:
540            self.private = True
541            pos, self.owner = self.get_object(message, str, pos)
542
543        if message[pos:] and self.private:
544            pos, numops = self.get_object(message, int, pos)
545
546            for _ in range(numops):
547                pos, operator = self.get_object(message, str, pos)
548
549                self.operators.append(operator)
550
551    def get_users(self, message):
552        pos, numusers = self.get_object(message, int)
553
554        users = []
555        for i in range(numusers):
556            users.append(UserData())
557            pos, users[i].username = self.get_object(message, str, pos)
558
559        pos, statuslen = self.get_object(message, int, pos)
560        for i in range(statuslen):
561            pos, users[i].status = self.get_object(message, int, pos)
562
563        pos, statslen = self.get_object(message, int, pos)
564        for i in range(statslen):
565            pos, users[i].avgspeed = self.get_object(message, int, pos)
566            pos, users[i].uploadnum = self.get_object(message, int, pos, getunsignedlonglong=True)
567            pos, users[i].files = self.get_object(message, int, pos)
568            pos, users[i].dirs = self.get_object(message, int, pos)
569
570        pos, slotslen = self.get_object(message, int, pos)
571        for i in range(slotslen):
572            pos, users[i].slotsfull = self.get_object(message, int, pos)
573
574        if message[pos:]:
575            pos, countrylen = self.get_object(message, int, pos)
576            for i in range(countrylen):
577                pos, users[i].country = self.get_object(message, str, pos)
578
579        return pos, users
580
581
582class LeaveRoom(ServerMessage):
583    """ Server code: 15 """
584    """ We send this to the server when we want to leave a room. """
585
586    def __init__(self, room=None):
587        self.room = room
588
589    def make_network_message(self):
590        return self.pack_object(self.room)
591
592    def parse_network_message(self, message):
593        _pos, self.room = self.get_object(message, str)
594
595
596class UserJoinedRoom(ServerMessage):
597    """ Server code: 16 """
598    """ The server tells us someone has just joined a room we're in. """
599
600    def __init__(self):
601        self.room = None
602        self.userdata = None
603
604    def parse_network_message(self, message):
605        pos, self.room = self.get_object(message, str)
606
607        self.userdata = UserData()
608        pos, self.userdata.username = self.get_object(message, str, pos)
609        pos, self.userdata.status = self.get_object(message, int, pos)
610        pos, self.userdata.avgspeed = self.get_object(message, int, pos)
611        pos, self.userdata.uploadnum = self.get_object(message, int, pos, getunsignedlonglong=True)
612        pos, self.userdata.files = self.get_object(message, int, pos)
613        pos, self.userdata.dirs = self.get_object(message, int, pos)
614        pos, self.userdata.slotsfull = self.get_object(message, int, pos)
615
616        # Soulfind support
617        if message[pos:]:
618            pos, self.userdata.country = self.get_object(message, str, pos)
619
620
621class UserLeftRoom(ServerMessage):
622    """ Server code: 17 """
623    """ The server tells us someone has just left a room we're in. """
624
625    def __init__(self):
626        self.room = None
627        self.username = None
628
629    def parse_network_message(self, message):
630        pos, self.room = self.get_object(message, str)
631        pos, self.username = self.get_object(message, str, pos)
632
633
634class ConnectToPeer(ServerMessage):
635    """ Server code: 18 """
636    """ Either we ask server to tell someone else we want to establish a
637    connection with them, or server tells us someone wants to connect with us.
638    Used when the side that wants a connection can't establish it, and tries
639    to go the other way around (direct connection has failed).
640    """
641
642    def __init__(self, token=None, user=None, conn_type=None):
643        self.token = token
644        self.user = user
645        self.conn_type = conn_type
646        self.ip_address = None
647        self.port = None
648        self.privileged = None
649
650    def make_network_message(self):
651        msg = bytearray()
652        msg.extend(self.pack_object(self.token))
653        msg.extend(self.pack_object(self.user))
654        msg.extend(self.pack_object(self.conn_type))
655
656        return msg
657
658    def parse_network_message(self, message):
659        pos, self.user = self.get_object(message, str)
660        pos, self.conn_type = self.get_object(message, str, pos)
661        pos, self.ip_address = pos + 4, socket.inet_ntoa(bytes(message[pos:pos + 4][::-1]))
662        pos, self.port = self.get_object(message, int, pos, 1)
663        pos, self.token = self.get_object(message, int, pos)
664
665        # Soulfind support
666        if message[pos:]:
667            pos, self.privileged = pos + 1, message[pos]
668
669
670class MessageUser(ServerMessage):
671    """ Server code: 22 """
672    """ Chat phrase sent to someone or received by us in private. """
673
674    def __init__(self, user=None, msg=None):
675        self.user = user
676        self.msg = msg
677        self.msgid = None
678        self.timestamp = None
679        self.newmessage = None
680
681    def make_network_message(self):
682        msg = bytearray()
683        msg.extend(self.pack_object(self.user))
684        msg.extend(self.pack_object(self.msg))
685
686        return msg
687
688    def parse_network_message(self, message):
689        pos, self.msgid = self.get_object(message, int)
690        pos, self.timestamp = self.get_object(message, int, pos)
691        pos, self.user = self.get_object(message, str, pos)
692        pos, self.msg = self.get_object(message, str, pos)
693
694        if message[pos:]:
695            pos, self.newmessage = pos + 1, message[pos]
696        else:
697            self.newmessage = 1
698
699
700class MessageAcked(ServerMessage):
701    """ Server code: 23 """
702    """ We send this to the server to confirm that we received a private message.
703    If we don't send it, the server will keep sending the chat phrase to us.
704    """
705
706    def __init__(self, msgid=None):
707        self.msgid = msgid
708
709    def make_network_message(self):
710        return self.pack_object(self.msgid)
711
712
713class FileSearch(ServerMessage):
714    """ Server code: 26 """
715    """ We send this to the server when we search for something. Alternatively,
716    the server sends this message outside the distributed network to tell us that
717    someone is searching for something, currently used for UserSearch and RoomSearch
718    requests.
719
720    The search id is a random number generated by the client and is used to track the
721    search results.
722    """
723
724    def __init__(self, requestid=None, text=None):
725        self.searchid = requestid
726        self.searchterm = text
727        self.user = None
728
729        if text:
730            self.searchterm = ' '.join((x for x in text.split() if x != '-'))
731
732    def make_network_message(self):
733        msg = bytearray()
734        msg.extend(self.pack_object(self.searchid))
735        msg.extend(self.pack_object(self.searchterm, latin1=True))
736
737        return msg
738
739    def parse_network_message(self, message):
740        pos, self.user = self.get_object(message, str)
741        pos, self.searchid = self.get_object(message, int, pos)
742        pos, self.searchterm = self.get_object(message, str, pos)
743
744
745class SetStatus(ServerMessage):
746    """ Server code: 28 """
747    """ We send our new status to the server. Status is a way to define whether
748    you're available or busy.
749
750    1 = Away
751    2 = Online
752    """
753
754    def __init__(self, status=None):
755        self.status = status
756
757    def make_network_message(self):
758        return self.pack_object(self.status)
759
760
761class ServerPing(ServerMessage):
762    """ Server code: 32 """
763    """ We test if the server responds. """
764    """ DEPRECATED """
765
766    def make_network_message(self):
767        return b""
768
769    def parse_network_message(self, message):
770        # Empty message
771        pass
772
773
774class SendConnectToken(ServerMessage):
775    """ Server code: 33 """
776    """ OBSOLETE, no longer used """
777
778    def __init__(self, user, token):
779        self.user = user
780        self.token = token
781
782    def make_network_message(self):
783        return self.pack_object(self.user) + self.pack_object(self.token)
784
785    def parse_network_message(self, message):
786        pos, self.user = self.get_object(message, str)
787        pos, self.token = self.get_object(message, int, pos)
788
789
790class SendDownloadSpeed(ServerMessage):
791    """ Server code: 34 """
792    """ We used to send this after a finished download to let the server update
793    the speed statistics for a user. """
794    """ OBSOLETE, use SendUploadSpeed server message """
795
796    def __init__(self, user=None, speed=None):
797        self.user = user
798        self.speed = speed
799
800    def make_network_message(self):
801        msg = bytearray()
802        msg.extend(self.pack_object(self.user))
803        msg.extend(self.pack_object(self.speed))
804
805        return msg
806
807
808class SharedFoldersFiles(ServerMessage):
809    """ Server code: 35 """
810    """ We send this to server to indicate the number of folder and files
811    that we share. """
812
813    def __init__(self, folders=None, files=None):
814        self.folders = folders
815        self.files = files
816
817    def make_network_message(self):
818        msg = bytearray()
819        msg.extend(self.pack_object(self.folders))
820        msg.extend(self.pack_object(self.files))
821
822        return msg
823
824
825class GetUserStats(ServerMessage):
826    """ Server code: 36 """
827    """ The server sends this to indicate a change in a user's statistics,
828    if we've requested to watch the user in AddUser previously. A user's
829    stats can also be requested by sending a GetUserStats message to the
830    server, but AddUser should be used instead. """
831
832    def __init__(self, user=None):
833        self.user = user
834        self.avgspeed = None
835        self.uploadnum = None
836        self.files = None
837        self.dirs = None
838
839    def make_network_message(self):
840        return self.pack_object(self.user)
841
842    def parse_network_message(self, message):
843        pos, self.user = self.get_object(message, str)
844        pos, self.avgspeed = self.get_object(message, int, pos)
845        pos, self.uploadnum = self.get_object(message, int, pos, getunsignedlonglong=True)
846        pos, self.files = self.get_object(message, int, pos)
847        pos, self.dirs = self.get_object(message, int, pos)
848
849
850class QueuedDownloads(ServerMessage):
851    """ Server code: 40 """
852    """ The server sends this to indicate if someone has download slots available
853    or not. """
854    """ OBSOLETE, no longer sent by the server """
855
856    def __init__(self):
857        self.user = None
858        self.slotsfull = None
859
860    def parse_network_message(self, message):
861        pos, self.user = self.get_object(message, str)
862        pos, self.slotsfull = self.get_object(message, int, pos)
863
864
865class Relogged(ServerMessage):
866    """ Server code: 41 """
867    """ The server sends this if someone else logged in under our nickname,
868    and then disconnects us. """
869
870    def parse_network_message(self, message):
871        # Empty message
872        pass
873
874
875class UserSearch(ServerMessage):
876    """ Server code: 42 """
877    """ We send this to the server when we search a specific user's shares.
878    The ticket/search id is a random number generated by the client and is
879    used to track the search results. """
880
881    def __init__(self, user=None, requestid=None, text=None):
882        self.user = user
883        self.searchid = requestid
884        self.searchterm = text
885
886    def make_network_message(self):
887        msg = bytearray()
888        msg.extend(self.pack_object(self.user))
889        msg.extend(self.pack_object(self.searchid))
890        msg.extend(self.pack_object(self.searchterm, latin1=True))
891
892        return msg
893
894    # Soulfind support, the official server sends a FileSearch message (code 26) instead
895    def parse_network_message(self, message):
896        pos, self.user = self.get_object(message, str)
897        pos, self.searchid = self.get_object(message, int, pos)
898        pos, self.searchterm = self.get_object(message, str, pos)
899
900
901class AddThingILike(ServerMessage):
902    """ Server code: 51 """
903    """ We send this to the server when we add an item to our likes list. """
904    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
905
906    def __init__(self, thing=None):
907        self.thing = thing
908
909    def make_network_message(self):
910        return self.pack_object(self.thing)
911
912
913class RemoveThingILike(ServerMessage):
914    """ Server code: 52 """
915    """ We send this to the server when we remove an item from our likes list. """
916    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
917
918    def __init__(self, thing=None):
919        self.thing = thing
920
921    def make_network_message(self):
922        return self.pack_object(self.thing)
923
924
925class Recommendations(ServerMessage):
926    """ Server code: 54 """
927    """ The server sends us a list of personal recommendations and a number
928    for each. """
929    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
930
931    def __init__(self):
932        self.recommendations = []
933        self.unrecommendations = []
934
935    def make_network_message(self):
936        return b""
937
938    def parse_network_message(self, message):
939        self.unpack_recommendations(message)
940
941    def unpack_recommendations(self, message, pos=0):
942        pos, num = self.get_object(message, int, pos)
943
944        for _ in range(num):
945            pos, key = self.get_object(message, str, pos)
946            pos, rating = self.get_object(message, int, pos, getsignedint=True)
947
948            # The server also includes unrecommendations here for some reason, don't add them
949            if rating >= 0:
950                self.recommendations.append((key, rating))
951
952        if not message[pos:]:
953            return
954
955        pos, num2 = self.get_object(message, int, pos)
956
957        for _ in range(num2):
958            pos, key = self.get_object(message, str, pos)
959            pos, rating = self.get_object(message, int, pos, getsignedint=True)
960
961            # The server also includes recommendations here for some reason, don't add them
962            if rating < 0:
963                self.unrecommendations.append((key, rating))
964
965
966class GlobalRecommendations(Recommendations):
967    """ Server code: 56 """
968    """ The server sends us a list of global recommendations and a number
969    for each. """
970    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
971
972
973class UserInterests(ServerMessage):
974    """ Server code: 57 """
975    """ We ask the server for a user's liked and hated interests. The server
976    responds with a list of interests. """
977    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
978
979    def __init__(self, user=None):
980        self.user = user
981        self.likes = []
982        self.hates = []
983
984    def make_network_message(self):
985        return self.pack_object(self.user)
986
987    def parse_network_message(self, message):
988        pos, self.user = self.get_object(message, str)
989        pos, likesnum = self.get_object(message, int, pos)
990
991        for _ in range(likesnum):
992            pos, key = self.get_object(message, str, pos)
993
994            self.likes.append(key)
995
996        pos, hatesnum = self.get_object(message, int, pos)
997
998        for _ in range(hatesnum):
999            pos, key = self.get_object(message, str, pos)
1000
1001            self.hates.append(key)
1002
1003
1004class AdminCommand(ServerMessage):
1005    """ Server code: 58 """
1006    """ We send this to the server to run an admin command (e.g. to ban or
1007    silence a user) if we have admin status on the server. """
1008    """ OBSOLETE, no longer used since Soulseek stopped supporting third-party
1009    servers in 2002 """
1010
1011    def __init__(self, command=None, command_args=None):
1012        self.command = command
1013        self.command_args = command_args
1014
1015    def make_network_message(self):
1016        msg = bytearray()
1017        msg.extend(self.pack_object(self.command))
1018        msg.extend(self.pack_object(len(self.command_args)))
1019
1020        for i in self.command_args:
1021            msg.extend(self.pack_object(i))
1022
1023        return msg
1024
1025
1026class PlaceInLineResponse(ServerMessage):
1027    """ Server code: 60 """
1028    """ The server sends this to indicate change in place in queue while we're
1029    waiting for files from another peer. """
1030    """ OBSOLETE, use PlaceInQueue peer message """
1031
1032    def __init__(self, user=None, req=None, place=None):
1033        self.req = req
1034        self.user = user
1035        self.place = place
1036
1037    def make_network_message(self):
1038        msg = bytearray()
1039        msg.extend(self.pack_object(self.user))
1040        msg.extend(self.pack_object(self.req))
1041        msg.extend(self.pack_object(self.place))
1042
1043        return msg
1044
1045    def parse_network_message(self, message):
1046        pos, self.user = self.get_object(message, str)
1047        pos, self.req = self.get_object(message, int, pos)
1048        pos, self.place = self.get_object(message, int, pos)
1049
1050
1051class RoomAdded(ServerMessage):
1052    """ Server code: 62 """
1053    """ The server tells us a new room has been added. """
1054    """ OBSOLETE, no longer sent by the server """
1055
1056    def __init__(self):
1057        self.room = None
1058
1059    def parse_network_message(self, message):
1060        _pos, self.room = self.get_object(message, str)
1061
1062
1063class RoomRemoved(ServerMessage):
1064    """ Server code: 63 """
1065    """ The server tells us a room has been removed. """
1066    """ OBSOLETE, no longer sent by the server """
1067
1068    def __init__(self):
1069        self.room = None
1070
1071    def parse_network_message(self, message):
1072        _pos, self.room = self.get_object(message, str)
1073
1074
1075class RoomList(ServerMessage):
1076    """ Server code: 64 """
1077    """ The server tells us a list of rooms and the number of users in
1078    them. When connecting to the server, the server only sends us rooms
1079    with at least 5 users. A few select rooms are also excluded, such as
1080    nicotine and The Lobby. Requesting the room list yields a response
1081    containing the missing rooms. """
1082
1083    def __init__(self):
1084        self.rooms = []
1085        self.ownedprivaterooms = []
1086        self.otherprivaterooms = []
1087
1088    def make_network_message(self):
1089        return b""
1090
1091    def parse_network_message(self, message):
1092        pos, numrooms = self.get_object(message, int)
1093
1094        for i in range(numrooms):
1095            pos, room = self.get_object(message, str, pos)
1096
1097            self.rooms.append([room, None])
1098
1099        pos, numusers = self.get_object(message, int, pos)
1100
1101        for i in range(numusers):
1102            pos, usercount = self.get_object(message, int, pos)
1103
1104            self.rooms[i][1] = usercount
1105
1106        if not message[pos:]:
1107            return
1108
1109        pos, self.ownedprivaterooms = self.get_rooms(pos, message)
1110        pos, self.otherprivaterooms = self.get_rooms(pos, message)
1111
1112    def get_rooms(self, originalpos, message):
1113        try:
1114            pos, numrooms = self.get_object(message, int, originalpos)
1115
1116            rooms = []
1117            for i in range(numrooms):
1118                pos, room = self.get_object(message, str, pos)
1119
1120                rooms.append([room, None])
1121
1122            pos, numusers = self.get_object(message, int, pos)
1123
1124            for i in range(numusers):
1125                pos, usercount = self.get_object(message, int, pos)
1126
1127                rooms[i][1] = usercount
1128
1129            return (pos, rooms)
1130
1131        except Exception as error:
1132            log.add("Exception during parsing %(area)s: %(exception)s", {'area': 'RoomList', 'exception': error})
1133            return (originalpos, [])
1134
1135
1136class ExactFileSearch(ServerMessage):
1137    """ Server code: 65 """
1138    """ We send this to search for an exact file name and folder,
1139    to find other sources. """
1140    """ OBSOLETE, no results even with official client """
1141
1142    def __init__(self, req=None, file=None, folder=None, size=None, checksum=None):
1143        self.req = req
1144        self.file = file
1145        self.folder = folder
1146        self.size = size
1147        self.checksum = checksum
1148        self.user = None
1149
1150    def make_network_message(self):
1151        msg = bytearray()
1152        msg.extend(self.pack_object(self.req))
1153        msg.extend(self.pack_object(self.file))
1154        msg.extend(self.pack_object(self.folder))
1155        msg.extend(self.pack_object(self.size, unsignedlonglong=True))
1156        msg.extend(self.pack_object(self.checksum))
1157
1158        return msg
1159
1160    def parse_network_message(self, message):
1161        pos, self.user = self.get_object(message, str)
1162        pos, self.req = self.get_object(message, int, pos)
1163        pos, self.file = self.get_object(message, str, pos)
1164        pos, self.folder = self.get_object(message, str, pos)
1165        pos, self.size = self.get_object(message, int, pos, getunsignedlonglong=True)
1166        pos, self.checksum = self.get_object(message, int, pos)
1167
1168
1169class AdminMessage(ServerMessage):
1170    """ Server code: 66 """
1171    """ A global message from the server admin has arrived. """
1172
1173    def __init__(self):
1174        self.msg = None
1175
1176    def parse_network_message(self, message):
1177        _pos, self.msg = self.get_object(message, str)
1178
1179
1180class GlobalUserList(ServerMessage):
1181    """ Server code: 67 """
1182    """ We send this to get a global list of all users online. """
1183    """ OBSOLETE, no longer used """
1184
1185    def __init__(self):
1186        self.users = None
1187
1188    def make_network_message(self):
1189        return b""
1190
1191    def parse_network_message(self, message):
1192        _pos, self.users = self.get_users(message)
1193
1194    def get_users(self, message):
1195        pos, numusers = self.get_object(message, int)
1196
1197        users = []
1198        for i in range(numusers):
1199            users.append(UserData())
1200            pos, users[i].username = self.get_object(message, str, pos)
1201
1202        pos, statuslen = self.get_object(message, int, pos)
1203        for i in range(statuslen):
1204            pos, users[i].status = self.get_object(message, int, pos)
1205
1206        pos, statslen = self.get_object(message, int, pos)
1207        for i in range(statslen):
1208            pos, users[i].avgspeed = self.get_object(message, int, pos)
1209            pos, users[i].uploadnum = self.get_object(message, int, pos, getunsignedlonglong=True)
1210            pos, users[i].files = self.get_object(message, int, pos)
1211            pos, users[i].dirs = self.get_object(message, int, pos)
1212
1213        pos, slotslen = self.get_object(message, int, pos)
1214        for i in range(slotslen):
1215            pos, users[i].slotsfull = self.get_object(message, int, pos)
1216
1217        if message[pos:]:
1218            pos, countrylen = self.get_object(message, int, pos)
1219            for i in range(countrylen):
1220                pos, users[i].country = self.get_object(message, str, pos)
1221
1222        return pos, users
1223
1224
1225class TunneledMessage(ServerMessage):
1226    """ Server code: 68 """
1227    """ Server message for tunneling a chat message. """
1228    """ OBSOLETE, no longer used """
1229
1230    def __init__(self, user=None, req=None, code=None, msg=None):
1231        self.user = user
1232        self.req = req
1233        self.code = code
1234        self.msg = msg
1235        self.addr = None
1236
1237    def make_network_message(self):
1238        msg = bytearray()
1239        msg.extend(self.pack_object(self.user))
1240        msg.extend(self.pack_object(self.req))
1241        msg.extend(self.pack_object(self.code))
1242        msg.extend(self.pack_object(self.msg))
1243
1244        return msg
1245
1246    def parse_network_message(self, message):
1247        pos, self.user = self.get_object(message, str)
1248        pos, self.code = self.get_object(message, int, pos)
1249        pos, self.req = self.get_object(message, int, pos)
1250
1251        pos, ip_address = pos + 4, socket.inet_ntoa(bytes(message[pos:pos + 4][::-1]))
1252        pos, port = self.get_object(message, int, pos, 1)
1253        self.addr = (ip_address, port)
1254
1255        pos, self.msg = self.get_object(message, str, pos)
1256
1257
1258class PrivilegedUsers(ServerMessage):
1259    """ Server code: 69 """
1260    """ The server sends us a list of privileged users, a.k.a. users who
1261    have donated. """
1262
1263    def __init__(self):
1264        self.users = []
1265
1266    def parse_network_message(self, message):
1267        pos, numusers = self.get_object(message, int)
1268
1269        for _ in range(numusers):
1270            pos, user = self.get_object(message, str, pos)
1271
1272            self.users.append(user)
1273
1274
1275class HaveNoParent(ServerMessage):
1276    """ Server code: 71 """
1277    """ We inform the server if we have a distributed parent or not.
1278    If not, the server eventually sends us a PossibleParents message with a
1279    list of 10 possible parents to connect to. """
1280
1281    def __init__(self, noparent=None):
1282        self.noparent = noparent
1283
1284    def make_network_message(self):
1285        return bytes([self.noparent])
1286
1287
1288class SearchParent(ServerMessage):
1289    """ Server code: 73 """
1290    """ We send the IP address of our parent to the server. """
1291    """ DEPRECATED, sent by Soulseek NS but not SoulseekQt """
1292
1293    def __init__(self, parentip=None):
1294        self.parentip = parentip
1295
1296    @staticmethod
1297    def strunreverse(string):
1298        strlist = string.split(".")
1299        strlist.reverse()
1300        return '.'.join(strlist)
1301
1302    def make_network_message(self):
1303        return self.pack_object(socket.inet_aton(self.strunreverse(self.parentip)))
1304
1305
1306class ParentMinSpeed(ServerMessage):
1307    """ Server code: 83 """
1308    """ The server informs us about the minimum upload speed required to become
1309    a parent in the distributed network. """
1310
1311    def __init__(self):
1312        self.speed = None
1313
1314    def parse_network_message(self, message):
1315        _pos, self.speed = self.get_object(message, int)
1316
1317
1318class ParentSpeedRatio(ServerMessage):
1319    """ Server code: 84 """
1320    """ The server sends us a speed ratio determining the number of children we
1321    can have in the distributed network. The maximum number of children is our
1322    upload speed divided by the speed ratio. """
1323
1324    def __init__(self):
1325        self.ratio = None
1326
1327    def parse_network_message(self, message):
1328        _pos, self.ratio = self.get_object(message, int)
1329
1330
1331class ParentInactivityTimeout(ServerMessage):
1332    """ Server code: 86 """
1333    """ OBSOLETE, no longer sent by the server """
1334
1335    def __init__(self):
1336        self.seconds = None
1337
1338    def parse_network_message(self, message):
1339        _pos, self.seconds = self.get_object(message, int)
1340
1341
1342class SearchInactivityTimeout(ServerMessage):
1343    """ Server code: 87 """
1344    """ OBSOLETE, no longer sent by the server """
1345
1346    def __init__(self):
1347        self.seconds = None
1348
1349    def parse_network_message(self, message):
1350        _pos, self.seconds = self.get_object(message, int)
1351
1352
1353class MinParentsInCache(ServerMessage):
1354    """ Server code: 88 """
1355    """ OBSOLETE, no longer sent by the server """
1356
1357    def __init__(self):
1358        self.num = None
1359
1360    def parse_network_message(self, message):
1361        _pos, self.num = self.get_object(message, int)
1362
1363
1364class DistribAliveInterval(ServerMessage):
1365    """ Server code: 90 """
1366    """ OBSOLETE, no longer sent by the server """
1367
1368    def __init__(self):
1369        self.seconds = None
1370
1371    def parse_network_message(self, message):
1372        _pos, self.seconds = self.get_object(message, int)
1373
1374
1375class AddToPrivileged(ServerMessage):
1376    """ Server code: 91 """
1377    """ The server sends us the username of a new privileged user, which we
1378    add to our list of global privileged users. """
1379    """ OBSOLETE, no longer sent by the server """
1380
1381    def __init__(self):
1382        self.user = None
1383
1384    def parse_network_message(self, message):
1385        _pos, self.user = self.get_object(message, str)
1386
1387
1388class CheckPrivileges(ServerMessage):
1389    """ Server code: 92 """
1390    """ We ask the server how much time we have left of our privileges.
1391    The server responds with the remaining time, in seconds. """
1392
1393    def __init__(self):
1394        self.seconds = None
1395
1396    def make_network_message(self):
1397        return b""
1398
1399    def parse_network_message(self, message):
1400        _pos, self.seconds = self.get_object(message, int)
1401
1402
1403class EmbeddedMessage(ServerMessage):
1404    """ Server code: 93 """
1405    """ The server sends us an embedded distributed message. The only type
1406    of distributed message sent at present is DistribSearch (distributed code 3).
1407    If we receive such a message, we are a branch root in the distributed network,
1408    and we distribute the embedded message (not the unpacked distributed message)
1409    to our child peers. """
1410
1411    __slots__ = ("distrib_code", "distrib_message")
1412
1413    def __init__(self):
1414        self.distrib_code = None
1415        self.distrib_message = None
1416
1417    def parse_network_message(self, message):
1418        self.distrib_code = message[0]
1419        self.distrib_message = message[1:]
1420
1421
1422class AcceptChildren(ServerMessage):
1423    """ Server code: 100 """
1424    """ We tell the server if we want to accept child nodes. """
1425
1426    def __init__(self, enabled=None):
1427        self.enabled = enabled
1428
1429    def make_network_message(self):
1430        return bytes([self.enabled])
1431
1432
1433class PossibleParents(ServerMessage):
1434    """ Server code: 102 """
1435    """ The server send us a list of 10 possible distributed parents to connect to.
1436    This message is sent to us at regular intervals until we tell the server we don't
1437    need more possible parents, through a HaveNoParent message. """
1438
1439    def __init__(self):
1440        self.list = {}
1441
1442    def parse_network_message(self, message):
1443        pos, num = self.get_object(message, int)
1444
1445        for _ in range(num):
1446            pos, username = self.get_object(message, str, pos)
1447            pos, ip_address = pos + 4, socket.inet_ntoa(bytes(message[pos:pos + 4][::-1]))
1448            pos, port = self.get_object(message, int, pos)
1449
1450            self.list[username] = (ip_address, port)
1451
1452
1453class WishlistSearch(FileSearch):
1454    """ Server code: 103 """
1455
1456
1457class WishlistInterval(ServerMessage):
1458    """ Server code: 104 """
1459
1460    def __init__(self):
1461        self.seconds = None
1462
1463    def parse_network_message(self, message):
1464        _pos, self.seconds = self.get_object(message, int)
1465
1466
1467class SimilarUsers(ServerMessage):
1468    """ Server code: 110 """
1469    """ The server sends us a list of similar users related to our interests. """
1470    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1471
1472    def __init__(self):
1473        self.users = []
1474
1475    def make_network_message(self):
1476        return b""
1477
1478    def parse_network_message(self, message):
1479        pos, num = self.get_object(message, int)
1480
1481        for _ in range(num):
1482            pos, user = self.get_object(message, str, pos)
1483            pos, _rating = self.get_object(message, int, pos)
1484
1485            self.users.append(user)
1486
1487
1488class ItemRecommendations(Recommendations):
1489    """ Server code: 111 """
1490    """ The server sends us a list of recommendations related to a specific
1491    item, which is usually present in the like/dislike list or an existing
1492    recommendation list. """
1493    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1494
1495    def __init__(self, thing=None):
1496        super().__init__()
1497        self.thing = thing
1498
1499    def make_network_message(self):
1500        return self.pack_object(self.thing)
1501
1502    def parse_network_message(self, message):
1503        pos, self.thing = self.get_object(message, str)
1504        self.unpack_recommendations(message, pos)
1505
1506
1507class ItemSimilarUsers(ServerMessage):
1508    """ Server code: 112 """
1509    """ The server sends us a list of similar users related to a specific item,
1510    which is usually present in the like/dislike list or recommendation list. """
1511    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1512
1513    def __init__(self, thing=None):
1514        self.thing = thing
1515        self.users = []
1516
1517    def make_network_message(self):
1518        return self.pack_object(self.thing)
1519
1520    def parse_network_message(self, message):
1521        pos, self.thing = self.get_object(message, str)
1522        pos, num = self.get_object(message, int, pos)
1523
1524        for _ in range(num):
1525            pos, user = self.get_object(message, str, pos)
1526            self.users.append(user)
1527
1528
1529class RoomTickerState(ServerMessage):
1530    """ Server code: 113 """
1531    """ The server returns a list of tickers in a chat room.
1532
1533    Tickers are customizable, user-specific messages that appear on
1534    chat room walls. """
1535
1536    def __init__(self):
1537        self.room = None
1538        self.user = None
1539        self.msgs = []
1540
1541    def parse_network_message(self, message):
1542        pos, self.room = self.get_object(message, str)
1543        pos, num = self.get_object(message, int, pos)
1544
1545        for _ in range(num):
1546            pos, user = self.get_object(message, str, pos)
1547            pos, msg = self.get_object(message, str, pos)
1548
1549            self.msgs.append((user, msg))
1550
1551
1552class RoomTickerAdd(ServerMessage):
1553    """ Server code: 114 """
1554    """ The server sends us a new ticker that was added to a chat room.
1555
1556    Tickers are customizable, user-specific messages that appear on
1557    chat room walls. """
1558
1559    def __init__(self):
1560        self.room = None
1561        self.user = None
1562        self.msg = None
1563
1564    def parse_network_message(self, message):
1565        pos, self.room = self.get_object(message, str)
1566        pos, self.user = self.get_object(message, str, pos)
1567        pos, self.msg = self.get_object(message, str, pos)
1568
1569
1570class RoomTickerRemove(ServerMessage):
1571    """ Server code: 115 """
1572    """ The server informs us that a ticker was removed from a chat room.
1573
1574    Tickers are customizable, user-specific messages that appear on
1575    chat room walls. """
1576
1577    def __init__(self):
1578        self.room = None
1579        self.user = None
1580
1581    def parse_network_message(self, message):
1582        pos, self.room = self.get_object(message, str)
1583        pos, self.user = self.get_object(message, str, pos)
1584
1585
1586class RoomTickerSet(ServerMessage):
1587    """ Server code: 116 """
1588    """ We send this to the server when we change our own ticker in
1589    a chat room. Sending an empty ticker string removes any existing
1590    ticker in the room.
1591
1592    Tickers are customizable, user-specific messages that appear on
1593    chat room walls. """
1594
1595    def __init__(self, room=None, msg=""):
1596        self.room = room
1597        self.msg = msg
1598
1599    def make_network_message(self):
1600        msg = bytearray()
1601        msg.extend(self.pack_object(self.room))
1602        msg.extend(self.pack_object(self.msg))
1603
1604        return msg
1605
1606
1607class AddThingIHate(AddThingILike):
1608    """ Server code: 117 """
1609    """ We send this to the server when we add an item to our hate list. """
1610    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1611
1612
1613class RemoveThingIHate(RemoveThingILike):
1614    """ Server code: 118 """
1615    """ We send this to the server when we remove an item from our hate list. """
1616    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1617
1618
1619class RoomSearch(ServerMessage):
1620    """ Server code: 120 """
1621    """ We send this to the server to search files shared by users who have joined
1622    a specific chat room. The ticket/search id is a random number generated by the
1623    client and is used to track the search results. """
1624
1625    def __init__(self, room=None, requestid=None, text=""):
1626        self.room = room
1627        self.searchid = requestid
1628        self.searchterm = ' '.join([x for x in text.split() if x != '-'])
1629        self.user = None
1630
1631    def make_network_message(self):
1632        msg = bytearray()
1633        msg.extend(self.pack_object(self.room))
1634        msg.extend(self.pack_object(self.searchid))
1635        msg.extend(self.pack_object(self.searchterm, latin1=True))
1636
1637        return msg
1638
1639    # Soulfind support, the official server sends a FileSearch message (code 26) instead
1640    def parse_network_message(self, message):
1641        pos, self.user = self.get_object(message, str)
1642        pos, self.searchid = self.get_object(message, int, pos)
1643        pos, self.searchterm = self.get_object(message, str, pos)
1644
1645
1646class SendUploadSpeed(ServerMessage):
1647    """ Server code: 121 """
1648    """ We send this after a finished upload to let the server update the speed
1649    statistics for ourselves. """
1650
1651    def __init__(self, speed=None):
1652        self.speed = speed
1653
1654    def make_network_message(self):
1655        return self.pack_object(self.speed)
1656
1657
1658class UserPrivileged(ServerMessage):
1659    """ Server code: 122 """
1660    """ We ask the server whether a user is privileged or not. """
1661    """ DEPRECATED, use AddUser and GetUserStatus server messages """
1662
1663    def __init__(self, user=None):
1664        self.user = user
1665        self.privileged = None
1666
1667    def make_network_message(self):
1668        return self.pack_object(self.user)
1669
1670    def parse_network_message(self, message):
1671        pos, self.user = self.get_object(message, str, 0)
1672        pos, self.privileged = pos + 1, bool(message[pos])
1673
1674
1675class GivePrivileges(ServerMessage):
1676    """ Server code: 123 """
1677    """ We give (part of) our privileges, specified in days, to another
1678    user on the network. """
1679
1680    def __init__(self, user=None, days=None):
1681        self.user = user
1682        self.days = days
1683
1684    def make_network_message(self):
1685        msg = bytearray()
1686        msg.extend(self.pack_object(self.user))
1687        msg.extend(self.pack_object(self.days))
1688
1689        return msg
1690
1691
1692class NotifyPrivileges(ServerMessage):
1693    """ Server code: 124 """
1694    """ DEPRECATED, no longer used """
1695
1696    def __init__(self, token=None, user=None):
1697        self.token = token
1698        self.user = user
1699
1700    def parse_network_message(self, message):
1701        pos, self.token = self.get_object(message, int)
1702        pos, self.user = self.get_object(message, str, pos)
1703
1704    def make_network_message(self):
1705        msg = bytearray()
1706        msg.extend(self.pack_object(self.token))
1707        msg.extend(self.pack_object(self.user))
1708
1709        return msg
1710
1711
1712class AckNotifyPrivileges(ServerMessage):
1713    """ Server code: 125 """
1714    """ DEPRECATED, no longer used """
1715
1716    def __init__(self, token=None):
1717        self.token = token
1718
1719    def parse_network_message(self, message):
1720        _pos, self.token = self.get_object(message, int)
1721
1722    def make_network_message(self):
1723        return self.pack_object(self.token)
1724
1725
1726class BranchLevel(ServerMessage):
1727    """ Server code: 126 """
1728    """ We tell the server what our position is in our branch (xth generation)
1729    on the distributed network. """
1730
1731    def __init__(self, value=None):
1732        self.value = value
1733
1734    def make_network_message(self):
1735        return self.pack_object(self.value)
1736
1737
1738class BranchRoot(ServerMessage):
1739    """ Server code: 127 """
1740    """ We tell the server the username of the root of the branch we’re in on
1741    the distributed network. """
1742
1743    def __init__(self, user=None):
1744        self.user = user
1745
1746    def make_network_message(self):
1747        return self.pack_object(self.user)
1748
1749
1750class ChildDepth(ServerMessage):
1751    """ Server code: 129 """
1752    """ We tell the server the maximum number of generation of children we
1753    have on the distributed network. """
1754
1755    def __init__(self, value=None):
1756        self.value = value
1757
1758    def make_network_message(self):
1759        return self.pack_object(self.value)
1760
1761
1762class ResetDistributed(ServerMessage):
1763    """ Server code: 130 """
1764    """ The server asks us to reset our distributed parent and children. """
1765
1766    def parse_network_message(self, message):
1767        # Empty message
1768        pass
1769
1770
1771class PrivateRoomUsers(ServerMessage):
1772    """ Server code: 133 """
1773    """ The server sends us a list of room users that we can alter
1774    (add operator abilities / dismember). """
1775
1776    def __init__(self):
1777        self.room = None
1778        self.numusers = None
1779        self.users = []
1780
1781    def parse_network_message(self, message):
1782        pos, self.room = self.get_object(message, str)
1783        pos, self.numusers = self.get_object(message, int, pos)
1784
1785        for _ in range(self.numusers):
1786            pos, user = self.get_object(message, str, pos)
1787
1788            self.users.append(user)
1789
1790
1791class PrivateRoomAddUser(ServerMessage):
1792    """ Server code: 134 """
1793    """ We send this to inform the server that we've added a user to a private room. """
1794
1795    def __init__(self, room=None, user=None):
1796        self.room = room
1797        self.user = user
1798
1799    def make_network_message(self):
1800        msg = bytearray()
1801        msg.extend(self.pack_object(self.room))
1802        msg.extend(self.pack_object(self.user))
1803
1804        return msg
1805
1806    def parse_network_message(self, message):
1807        pos, self.room = self.get_object(message, str)
1808        pos, self.user = self.get_object(message, str, pos)
1809
1810
1811class PrivateRoomRemoveUser(PrivateRoomAddUser):
1812    """ Server code: 135 """
1813    """ We send this to inform the server that we've removed a user from a private room. """
1814
1815
1816class PrivateRoomDismember(ServerMessage):
1817    """ Server code: 136 """
1818    """ We send this to the server to remove our own membership of a private room. """
1819
1820    def __init__(self, room=None):
1821        self.room = room
1822
1823    def make_network_message(self):
1824        return self.pack_object(self.room)
1825
1826
1827class PrivateRoomDisown(ServerMessage):
1828    """ Server code: 137 """
1829    """ We send this to the server to stop owning a private room. """
1830
1831    def __init__(self, room=None):
1832        self.room = room
1833
1834    def make_network_message(self):
1835        return self.pack_object(self.room)
1836
1837
1838class PrivateRoomSomething(ServerMessage):
1839    """ Server code: 138 """
1840    """ OBSOLETE, no longer used """
1841
1842    def __init__(self, room=None):
1843        self.room = room
1844
1845    def make_network_message(self):
1846        return self.pack_object(self.room)
1847
1848    def parse_network_message(self, message):
1849        _pos, self.room = self.get_object(message, str)
1850
1851
1852class PrivateRoomAdded(ServerMessage):
1853    """ Server code: 139 """
1854    """ The server sends us this message when we are added to a private room. """
1855
1856    def __init__(self, room=None):
1857        self.room = room
1858
1859    def parse_network_message(self, message):
1860        _pos, self.room = self.get_object(message, str)
1861
1862
1863class PrivateRoomRemoved(PrivateRoomAdded):
1864    """ Server code: 140 """
1865    """ The server sends us this message when we are removed from a private room. """
1866
1867
1868class PrivateRoomToggle(ServerMessage):
1869    """ Server code: 141 """
1870    """ We send this when we want to enable or disable invitations to private rooms. """
1871
1872    def __init__(self, enabled=None):
1873        self.enabled = None if enabled is None else int(enabled)
1874
1875    def make_network_message(self):
1876        return bytes([self.enabled])
1877
1878    def parse_network_message(self, message):
1879        # When this is received, we store it in the config, and disable the appropriate menu item
1880        self.enabled = bool(int(message[0]))
1881
1882
1883class ChangePassword(ServerMessage):
1884    """ Server code: 142 """
1885    """ We send this to the server to change our password. We receive a
1886    response if our password changes. """
1887
1888    def __init__(self, password=None):
1889        self.password = password
1890
1891    def make_network_message(self):
1892        return self.pack_object(self.password)
1893
1894    def parse_network_message(self, message):
1895        _pos, self.password = self.get_object(message, str)
1896
1897
1898class PrivateRoomAddOperator(PrivateRoomAddUser):
1899    """ Server code: 143 """
1900    """ We send this to the server to add private room operator abilities to a user. """
1901
1902
1903class PrivateRoomRemoveOperator(PrivateRoomAddUser):
1904    """ Server code: 144 """
1905    """ We send this to the server to remove private room operator abilities from a user. """
1906
1907
1908class PrivateRoomOperatorAdded(ServerMessage):
1909    """ Server code: 145 """
1910    """ The server send us this message when we're given operator abilities
1911    in a private room. """
1912
1913    def __init__(self, room=None):
1914        self.room = room
1915
1916    def parse_network_message(self, message):
1917        _pos, self.room = self.get_object(message, str)
1918
1919
1920class PrivateRoomOperatorRemoved(ServerMessage):
1921    """ Server code: 146 """
1922    """ The server send us this message when our operator abilities are removed
1923    in a private room. """
1924
1925    def __init__(self, room=None):
1926        self.room = room
1927
1928    def make_network_message(self):
1929        return self.pack_object(self.room)
1930
1931    def parse_network_message(self, message):
1932        _pos, self.room = self.get_object(message, str)
1933
1934
1935class PrivateRoomOwned(ServerMessage):
1936    """ Server code: 148 """
1937    """ The server sends us a list of operators in a specific room, that we can
1938    remove operator abilities from. """
1939
1940    def __init__(self):
1941        self.room = None
1942        self.number = None
1943        self.operators = []
1944
1945    def parse_network_message(self, message):
1946        pos, self.room = self.get_object(message, str)
1947        pos, self.number = self.get_object(message, int, pos)
1948
1949        for _ in range(self.number):
1950            pos, user = self.get_object(message, str, pos)
1951
1952            self.operators.append(user)
1953
1954
1955class MessageUsers(ServerMessage):
1956    """ Server code: 149 """
1957    """ Sends a broadcast private message to the given list of users. """
1958
1959    def __init__(self, users=None, msg=None):
1960        self.users = users
1961        self.msg = msg
1962
1963    def make_network_message(self):
1964        msg = bytearray()
1965        msg.extend(self.pack_object(len(self.users)))
1966
1967        for user in self.users:
1968            msg.extend(self.pack_object(user))
1969
1970        msg.extend(self.pack_object(self.msg))
1971
1972
1973class JoinPublicRoom(ServerMessage):
1974    """ Server code: 150 """
1975    """ We ask the server to send us messages from all public rooms, also
1976    known as public chat. """
1977    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1978
1979    def make_network_message(self):
1980        return b""
1981
1982
1983class LeavePublicRoom(ServerMessage):
1984    """ Server code: 151 """
1985    """ We ask the server to stop sending us messages from all public rooms,
1986    also known as public chat. """
1987    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1988
1989    def make_network_message(self):
1990        return b""
1991
1992
1993class PublicRoomMessage(ServerMessage):
1994    """ Server code: 152 """
1995    """ The server sends this when a new message has been written in a public
1996    room (every single line written in every public room). """
1997    """ DEPRECATED, used in Soulseek NS but not SoulseekQt """
1998
1999    def __init__(self):
2000        self.room = None
2001        self.user = None
2002        self.msg = None
2003
2004    def parse_network_message(self, message):
2005        pos, self.room = self.get_object(message, str)
2006        pos, self.user = self.get_object(message, str, pos)
2007        pos, self.msg = self.get_object(message, str, pos)
2008
2009
2010class RelatedSearch(ServerMessage):
2011    """ Server code: 153 """
2012    """ The server returns a list of related search terms for a search query. """
2013    """ OBSOLETE, server sends empty list as of 2018 """
2014
2015    def __init__(self, query=None):
2016        self.query = query
2017        self.terms = []
2018
2019    def make_network_message(self):
2020        return self.pack_object(self.query)
2021
2022    def parse_network_message(self, message):
2023        pos, self.query = self.get_object(message, str)
2024        pos, num = self.get_object(message, int, pos)
2025
2026        for _ in range(num):
2027            pos, term = self.get_object(message, str, pos)
2028            pos, score = self.get_object(message, int, pos)
2029
2030            self.terms.append((term, score))
2031
2032
2033class CantConnectToPeer(ServerMessage):
2034    """ Server code: 1001 """
2035    """ We send this to say we can't connect to peer after it has asked us
2036    to connect. We receive this if we asked peer to connect and it can't do
2037    this. This message means a connection can't be established either way. """
2038
2039    def __init__(self, token=None, user=None):
2040        self.token = token
2041        self.user = user
2042
2043    def make_network_message(self):
2044        msg = bytearray()
2045        msg.extend(self.pack_object(self.token))
2046        msg.extend(self.pack_object(self.user))
2047
2048        return msg
2049
2050    def parse_network_message(self, message):
2051        _pos, self.token = self.get_object(message, int)
2052
2053
2054class CantCreateRoom(ServerMessage):
2055    """ Server code: 1003 """
2056    """ Server tells us a new room cannot be created. This message only seems
2057    to be sent if you try to create a room with the same name as an existing
2058    private room. In other cases, such as using a room name with leading or
2059    trailing spaces, only a private message containing an error message is sent. """
2060
2061    def __init__(self):
2062        self.room = None
2063
2064    def parse_network_message(self, message):
2065        _pos, self.room = self.get_object(message, str)
2066
2067
2068"""
2069Peer Init Messages
2070"""
2071
2072
2073class PeerInitMessage(SlskMessage):
2074    pass
2075
2076
2077class PierceFireWall(PeerInitMessage):
2078    """ Peer init code: 0 """
2079    """ This is the very first message sent by the peer that established a
2080    connection, if it has been asked by the other peer to do so. The token
2081    is taken from the ConnectToPeer server message. """
2082
2083    def __init__(self, conn=None, token=None):
2084        self.conn = conn
2085        self.token = token
2086
2087    def make_network_message(self):
2088        return self.pack_object(self.token)
2089
2090    def parse_network_message(self, message):
2091        if message:
2092            # A token is not guaranteed to be sent
2093            _pos, self.token = self.get_object(message, int)
2094
2095
2096class PeerInit(PeerInitMessage):
2097    """ Peer init code: 1 """
2098    """ This message is sent by the peer that initiated a connection,
2099    not necessarily a peer that actually established it. Token apparently
2100    can be anything. Type is 'P' if it's anything but filetransfer,
2101    'F' otherwise. """
2102
2103    def __init__(self, conn=None, init_user=None, target_user=None, conn_type=None, token=None):
2104        self.conn = conn
2105        self.init_user = init_user      # username of peer who initiated the message
2106        self.target_user = target_user  # username of peer we're connected to
2107        self.conn_type = conn_type
2108        self.token = token
2109
2110    def make_network_message(self):
2111        msg = bytearray()
2112        msg.extend(self.pack_object(self.init_user))
2113        msg.extend(self.pack_object(self.conn_type))
2114        msg.extend(self.pack_object(self.token))
2115
2116        return msg
2117
2118    def parse_network_message(self, message):
2119        pos, self.init_user = self.get_object(message, str)
2120        pos, self.conn_type = self.get_object(message, str, pos)
2121
2122        if message[pos:]:
2123            # A token is not guaranteed to be sent
2124            pos, self.token = self.get_object(message, int, pos)
2125
2126        if self.target_user is None:
2127            # The user we're connecting to initiated the connection. Set them as target user.
2128            self.target_user = self.init_user
2129
2130
2131"""
2132Peer Messages
2133"""
2134
2135
2136class PeerMessage(SlskMessage):
2137
2138    def parse_file_size(self, message, pos):
2139
2140        if message[pos + INT64_SIZE - 1] == 255:
2141            # Soulseek NS bug: >2 GiB files show up as ~16 EiB when unpacking the size
2142            # as uint64 (8 bytes), due to the first 4 bytes containing the size, and the
2143            # last 4 bytes containing garbage (a value of 4294967295 bytes, integer limit).
2144            # Only unpack the first 4 bytes to work around this issue.
2145
2146            pos, size = self.get_object(message, int, pos)
2147            pos, _garbage = self.get_object(message, int, pos)
2148
2149        else:
2150            # Everything looks fine, parse size as usual
2151            pos, size = self.get_object(message, int, pos, getunsignedlonglong=True)
2152
2153        return pos, size
2154
2155
2156class GetSharedFileList(PeerMessage):
2157    """ Peer code: 4 """
2158    """ We send this to a peer to ask for a list of shared files. """
2159
2160    def __init__(self, conn):
2161        self.conn = conn
2162
2163    def make_network_message(self):
2164        return b""
2165
2166    def parse_network_message(self, message):
2167        # Empty message
2168        pass
2169
2170
2171class SharedFileList(PeerMessage):
2172    """ Peer code: 5 """
2173    """ A peer responds with a list of shared files when we've sent
2174    a GetSharedFileList. """
2175
2176    def __init__(self, conn, shares=None):
2177        self.conn = conn
2178        self.list = shares
2179        self.unknown = 0
2180        self.privatelist = []
2181        self.built = None
2182
2183    def parse_network_message(self, message):
2184        try:
2185            message = memoryview(zlib.decompress(message))
2186            self._parse_network_message(message)
2187
2188        except Exception as error:
2189            log.add("Exception during parsing %(area)s: %(exception)s",
2190                    {'area': 'SharedFileList', 'exception': error})
2191            self.list = []
2192            self.privatelist = []
2193
2194    def _parse_result_list(self, message, pos=0):
2195        pos, ndir = self.get_object(message, int, pos)
2196
2197        shares = []
2198        for _ in range(ndir):
2199            pos, directory = self.get_object(message, str, pos)
2200            directory = directory.replace('/', '\\')
2201            pos, nfiles = self.get_object(message, int, pos)
2202
2203            files = []
2204
2205            for _ in range(nfiles):
2206                pos, code = pos + 1, message[pos]
2207                pos, name = self.get_object(message, str, pos)
2208                pos, size = self.parse_file_size(message, pos)
2209                pos, ext = self.get_object(message, str, pos)
2210                pos, numattr = self.get_object(message, int, pos)
2211
2212                attrs = []
2213
2214                for _ in range(numattr):
2215                    pos, _attrnum = self.get_object(message, int, pos)
2216                    pos, attr = self.get_object(message, int, pos)
2217                    attrs.append(attr)
2218
2219                files.append((code, name, size, ext, attrs))
2220
2221            shares.append((directory, files))
2222
2223        return pos, shares
2224
2225    def _parse_network_message(self, message):
2226        pos, self.list = self._parse_result_list(message)
2227
2228        if message[pos:]:
2229            pos, self.unknown = self.get_object(message, int, pos)
2230
2231        if message[pos:]:
2232            pos, self.privatelist = self._parse_result_list(message, pos)
2233
2234    def make_network_message(self):
2235        # Elaborate hack to save CPU
2236        # Store packed message contents in self.built, and use instead of repacking it
2237        if self.built is not None:
2238            return self.built
2239
2240        msg = bytearray()
2241        msg_list = bytearray()
2242
2243        if self.list is None:
2244            # DB is closed
2245            msg_list = self.pack_object(0)
2246
2247        else:
2248            try:
2249                try:
2250                    msg_list.extend(self.pack_object(len(self.list)))
2251
2252                except TypeError:
2253                    msg_list.extend(self.pack_object(len(list(self.list))))
2254
2255                for key in self.list:
2256                    msg_list.extend(self.pack_object(key.replace('/', '\\')))
2257                    msg_list.extend(self.list[key])
2258
2259            except Exception as error:
2260                msg_list = self.pack_object(0)
2261                log.add(_("Unable to read shares database. Please rescan your shares. Error: %s"), error)
2262
2263        msg.extend(msg_list)
2264
2265        # Unknown purpose, but official clients always send a value of 0
2266        msg.extend(self.pack_object(self.unknown))
2267
2268        self.built = zlib.compress(msg)
2269        return self.built
2270
2271
2272class FileSearchRequest(PeerMessage):
2273    """ Peer code: 8 """
2274    """ We send this to the peer when we search for a file.
2275    Alternatively, the peer sends this to tell us it is
2276    searching for a file. """
2277    """ OBSOLETE, use UserSearch server message """
2278
2279    def __init__(self, conn, requestid=None, text=None):
2280        self.conn = conn
2281        self.requestid = requestid
2282        self.text = text
2283        self.searchid = None
2284        self.searchterm = None
2285
2286    def make_network_message(self):
2287        msg = bytearray()
2288        msg.extend(self.pack_object(self.requestid))
2289        msg.extend(self.pack_object(self.text))
2290
2291        return msg
2292
2293    def parse_network_message(self, message):
2294        pos, self.searchid = self.get_object(message, int)
2295        pos, self.searchterm = self.get_object(message, str, pos)
2296
2297
2298class FileSearchResult(PeerMessage):
2299    """ Peer code: 9 """
2300    """ A peer sends this message when it has a file search match. The
2301    token/ticket is taken from original FileSearch, UserSearch or
2302    RoomSearch message. """
2303
2304    __slots__ = ("conn", "user", "geoip", "token", "list", "privatelist", "freeulslots",
2305                 "ulspeed", "inqueue", "fifoqueue")
2306
2307    def __init__(self, conn=None, user=None, token=None, shares=None, freeulslots=None,
2308                 ulspeed=None, inqueue=None, fifoqueue=None):
2309        self.conn = conn
2310        self.user = user
2311        self.token = token
2312        self.list = shares
2313        self.privatelist = []
2314        self.freeulslots = freeulslots
2315        self.ulspeed = ulspeed
2316        self.inqueue = inqueue
2317        self.fifoqueue = fifoqueue
2318
2319    def parse_network_message(self, message):
2320        try:
2321            message = memoryview(zlib.decompress(message))
2322            self._parse_network_message(message)
2323
2324        except Exception as error:
2325            log.add("Exception during parsing %(area)s: %(exception)s",
2326                    {'area': 'FileSearchResult', 'exception': error})
2327            self.list = []
2328            self.privatelist = []
2329
2330    def _parse_result_list(self, message, pos):
2331        pos, nfiles = self.get_object(message, int, pos)
2332
2333        shares = []
2334        for _ in range(nfiles):
2335            pos, code = pos + 1, message[pos]
2336            pos, name = self.get_object(message, str, pos)
2337            pos, size = self.parse_file_size(message, pos)
2338            pos, ext = self.get_object(message, str, pos)
2339            pos, numattr = self.get_object(message, int, pos)
2340
2341            attrs = []
2342            if numattr:
2343                for _ in range(numattr):
2344                    pos, _attrnum = self.get_object(message, int, pos)
2345                    pos, attr = self.get_object(message, int, pos)
2346                    attrs.append(attr)
2347
2348            shares.append((code, name.replace('/', '\\'), size, ext, attrs))
2349
2350        return pos, shares
2351
2352    def _parse_network_message(self, message):
2353        pos, self.user = self.get_object(message, str)
2354        pos, self.token = self.get_object(message, int, pos)
2355
2356        if self.token not in SEARCH_TOKENS_ALLOWED:
2357            # Results are no longer accepted for this search token, stop parsing message
2358            self.list = []
2359            return
2360
2361        pos, self.list = self._parse_result_list(message, pos)
2362
2363        pos, self.freeulslots = pos + 1, message[pos]
2364        pos, self.ulspeed = self.get_object(message, int, pos)
2365        pos, self.inqueue = self.get_object(message, int, pos, getunsignedlonglong=True)
2366
2367        if message[pos:] and config.sections["searches"]["private_search_results"]:
2368            pos, self.privatelist = self._parse_result_list(message, pos)
2369
2370    def pack_file_info(self, fileinfo):
2371        msg = bytearray()
2372        msg.extend(bytes([1]))
2373        msg.extend(self.pack_object(fileinfo[0].replace('/', '\\')))
2374        msg.extend(self.pack_object(fileinfo[1], unsignedlonglong=True))
2375
2376        if fileinfo[2] is None or fileinfo[3] is None:
2377            # No metadata
2378            msg.extend(self.pack_object(''))
2379            msg.extend(self.pack_object(0))
2380        else:
2381            # FileExtension, NumAttributes
2382            msg.extend(self.pack_object("mp3"))
2383            msg.extend(self.pack_object(3))
2384
2385            # Length
2386            msg.extend(self.pack_object(0))
2387            msg.extend(self.pack_object(fileinfo[2][0] or 0))
2388
2389            # Duration
2390            msg.extend(self.pack_object(1))
2391            msg.extend(self.pack_object(fileinfo[3] or 0))
2392
2393            # VBR
2394            msg.extend(self.pack_object(2))
2395            msg.extend(self.pack_object(fileinfo[2][1] or 0))
2396
2397        return msg
2398
2399    def make_network_message(self):
2400        msg = bytearray()
2401        msg.extend(self.pack_object(self.user))
2402        msg.extend(self.pack_object(self.token))
2403        msg.extend(self.pack_object(len(self.list)))
2404
2405        for fileinfo in self.list:
2406            msg.extend(self.pack_file_info(fileinfo))
2407
2408        msg.extend(bytes([self.freeulslots]))
2409        msg.extend(self.pack_object(self.ulspeed))
2410        msg.extend(self.pack_object(self.inqueue, unsignedlonglong=True))
2411
2412        return zlib.compress(msg)
2413
2414
2415class UserInfoRequest(PeerMessage):
2416    """ Peer code: 15 """
2417    """ We ask the other peer to send us their user information, picture
2418    and all."""
2419
2420    def __init__(self, conn):
2421        self.conn = conn
2422
2423    def make_network_message(self):
2424        return b""
2425
2426    def parse_network_message(self, message):
2427        # Empty message
2428        pass
2429
2430
2431class UserInfoReply(PeerMessage):
2432    """ Peer code: 16 """
2433    """ A peer responds with this when we've sent a UserInfoRequest. """
2434
2435    def __init__(self, conn, descr=None, pic=None, totalupl=None, queuesize=None, slotsavail=None, uploadallowed=None):
2436        self.conn = conn
2437        self.descr = descr
2438        self.pic = pic
2439        self.totalupl = totalupl
2440        self.queuesize = queuesize
2441        self.slotsavail = slotsavail
2442        self.uploadallowed = uploadallowed
2443        self.has_pic = None
2444
2445    def parse_network_message(self, message):
2446        pos, self.descr = self.get_object(message, str)
2447        pos, self.has_pic = pos + 1, message[pos]
2448
2449        if self.has_pic:
2450            pos, self.pic = self.get_object(message, bytes, pos)
2451
2452        pos, self.totalupl = self.get_object(message, int, pos)
2453        pos, self.queuesize = self.get_object(message, int, pos)
2454        pos, self.slotsavail = pos + 1, message[pos]
2455
2456        # To prevent errors, ensure that >= 4 bytes are left. Museek+ incorrectly sends
2457        # slotsavail as an integer, resulting in 3 bytes of garbage here.
2458        if len(message[pos:]) >= 4:
2459            pos, self.uploadallowed = self.get_object(message, int, pos)
2460
2461    def make_network_message(self):
2462        msg = bytearray()
2463        msg.extend(self.pack_object(self.descr))
2464
2465        if self.pic is not None:
2466            msg.extend(bytes([1]))
2467            msg.extend(self.pack_object(self.pic))
2468        else:
2469            msg.extend(bytes([0]))
2470
2471        msg.extend(self.pack_object(self.totalupl))
2472        msg.extend(self.pack_object(self.queuesize))
2473        msg.extend(bytes([self.slotsavail]))
2474        msg.extend(self.pack_object(self.uploadallowed))
2475
2476        return msg
2477
2478
2479class PMessageUser(PeerMessage):
2480    """ Peer code: 22 """
2481    """ Chat phrase sent to someone or received by us in private.
2482    This is a Nicotine+ extension to the Soulseek protocol. """
2483    """ DEPRECATED """
2484
2485    def __init__(self, conn=None, user=None, msg=None):
2486        self.conn = conn
2487        self.user = user
2488        self.msg = msg
2489        self.msgid = None
2490        self.timestamp = None
2491
2492    def make_network_message(self):
2493        msg = bytearray()
2494        msg.extend(self.pack_object(0))
2495        msg.extend(self.pack_object(0))
2496        msg.extend(self.pack_object(self.user))
2497        msg.extend(self.pack_object(self.msg))
2498
2499        return msg
2500
2501    def parse_network_message(self, message):
2502        pos, self.msgid = self.get_object(message, int)
2503        pos, self.timestamp = self.get_object(message, int, pos)
2504        pos, self.user = self.get_object(message, str, pos)
2505        pos, self.msg = self.get_object(message, str, pos)
2506
2507
2508class FolderContentsRequest(PeerMessage):
2509    """ Peer code: 36 """
2510    """ We ask the peer to send us the contents of a single folder. """
2511
2512    def __init__(self, conn, directory=None):
2513        self.conn = conn
2514        self.dir = directory
2515        self.something = None
2516
2517    def make_network_message(self):
2518        msg = bytearray()
2519        msg.extend(self.pack_object(1))
2520        msg.extend(self.pack_object(self.dir, latin1=True))
2521
2522        return msg
2523
2524    def parse_network_message(self, message):
2525        pos, self.something = self.get_object(message, int)
2526        pos, self.dir = self.get_object(message, str, pos)
2527
2528
2529class FolderContentsResponse(PeerMessage):
2530    """ Peer code: 37 """
2531    """ A peer responds with the contents of a particular folder
2532    (with all subfolders) when we've sent a FolderContentsRequest. """
2533
2534    def __init__(self, conn, directory=None, shares=None):
2535        self.conn = conn
2536        self.dir = directory
2537        self.list = shares
2538
2539    def parse_network_message(self, message):
2540        try:
2541            message = memoryview(zlib.decompress(message))
2542            self._parse_network_message(message)
2543
2544        except Exception as error:
2545            log.add("Exception during parsing %(area)s: %(exception)s",
2546                    {'area': 'FolderContentsResponse', 'exception': error})
2547            self.list = {}
2548
2549    def _parse_network_message(self, message):
2550        shares = {}
2551        pos, nfolders = self.get_object(message, int)
2552
2553        for _ in range(nfolders):
2554            pos, folder = self.get_object(message, str, pos)
2555
2556            shares[folder] = {}
2557
2558            pos, ndir = self.get_object(message, int, pos)
2559
2560            for _ in range(ndir):
2561                pos, directory = self.get_object(message, str, pos)
2562                directory = directory.replace('/', '\\')
2563                pos, nfiles = self.get_object(message, int, pos)
2564
2565                shares[folder][directory] = []
2566
2567                for _ in range(nfiles):
2568                    pos, code = pos + 1, message[pos]
2569                    pos, name = self.get_object(message, str, pos)
2570                    pos, size = self.get_object(message, int, pos, getunsignedlonglong=True)
2571                    pos, ext = self.get_object(message, str, pos)
2572                    pos, numattr = self.get_object(message, int, pos)
2573
2574                    attrs = []
2575
2576                    for _ in range(numattr):
2577                        pos, _attrnum = self.get_object(message, int, pos)
2578                        pos, attr = self.get_object(message, int, pos)
2579                        attrs.append(attr)
2580
2581                    shares[folder][directory].append((code, name, size, ext, attrs))
2582
2583        self.list = shares
2584
2585    def make_network_message(self):
2586        msg = bytearray()
2587        msg.extend(self.pack_object(1))
2588        msg.extend(self.pack_object(self.dir))
2589        msg.extend(self.pack_object(1))
2590        msg.extend(self.pack_object(self.dir))
2591
2592        if self.list is not None:
2593            # We already saved the folder contents as a bytearray when scanning our shares
2594            msg.extend(self.list)
2595        else:
2596            # No folder contents
2597            msg.extend(self.pack_object(0))
2598
2599        return zlib.compress(msg)
2600
2601
2602class TransferRequest(PeerMessage):
2603    """ Peer code: 40 """
2604    """ This message is sent by a peer once they are ready to start uploading a file.
2605    A TransferResponse message is expected from the recipient, either allowing or
2606    rejecting the upload attempt.
2607
2608    This message was formely used to send a download request (direction 0) as well,
2609    but Nicotine+, Museek+ and the official clients use the QueueUpload message for
2610    this purpose today. """
2611
2612    def __init__(self, conn, direction=None, req=None, file=None, filesize=None, realfile=None):
2613        self.conn = conn
2614        self.direction = direction
2615        self.req = req
2616        self.file = file  # virtual file
2617        self.realfile = realfile
2618        self.filesize = filesize
2619
2620    def make_network_message(self):
2621        msg = bytearray()
2622        msg.extend(self.pack_object(self.direction))
2623        msg.extend(self.pack_object(self.req))
2624        msg.extend(self.pack_object(self.file))
2625
2626        if self.filesize is not None and self.direction == 1:
2627            msg.extend(self.pack_object(self.filesize, unsignedlonglong=True))
2628
2629        return msg
2630
2631    def parse_network_message(self, message):
2632        pos, self.direction = self.get_object(message, int)
2633        pos, self.req = self.get_object(message, int, pos)
2634        pos, self.file = self.get_object(message, str, pos)
2635
2636        if self.direction == 1:
2637            pos, self.filesize = self.get_object(message, int, pos, getunsignedlonglong=True)
2638
2639
2640class TransferResponse(PeerMessage):
2641    """ Peer code: 41 """
2642    """ Response to TransferRequest - either we (or the other peer) agrees,
2643    or tells the reason for rejecting the file transfer. """
2644
2645    def __init__(self, conn, allowed=None, reason=None, req=None, filesize=None):
2646        self.conn = conn
2647        self.allowed = allowed
2648        self.req = req
2649        self.reason = reason
2650        self.filesize = filesize
2651
2652    def make_network_message(self):
2653        msg = bytearray()
2654        msg.extend(self.pack_object(self.req))
2655        msg.extend(bytes([self.allowed]))
2656
2657        if self.reason is not None:
2658            msg.extend(self.pack_object(self.reason))
2659
2660        if self.filesize is not None:
2661            msg.extend(self.pack_object(self.filesize, unsignedlonglong=True))
2662
2663        return msg
2664
2665    def parse_network_message(self, message):
2666        pos, self.req = self.get_object(message, int)
2667        pos, self.allowed = pos + 1, message[pos]
2668
2669        if message[pos:]:
2670            if self.allowed:
2671                pos, self.filesize = self.get_object(message, int, pos, getunsignedlonglong=True)
2672            else:
2673                pos, self.reason = self.get_object(message, str, pos)
2674
2675
2676class PlaceholdUpload(PeerMessage):
2677    """ Peer code: 42 """
2678    """ OBSOLETE, no longer used """
2679
2680    def __init__(self, conn, file=None):
2681        self.conn = conn
2682        self.file = file
2683
2684    def make_network_message(self):
2685        return self.pack_object(self.file)
2686
2687    def parse_network_message(self, message):
2688        _pos, self.file = self.get_object(message, str)
2689
2690
2691class QueueUpload(PeerMessage):
2692    """ Peer code: 43 """
2693    """ This message is used to tell a peer that an upload should be queued on their end.
2694    Once the recipient is ready to transfer the requested file, they will send an upload
2695    request. """
2696
2697    def __init__(self, conn, file=None, legacy_client=False):
2698        self.conn = conn
2699        self.file = file
2700        self.legacy_client = legacy_client
2701
2702    def make_network_message(self):
2703        return self.pack_object(self.file, latin1=self.legacy_client)
2704
2705    def parse_network_message(self, message):
2706        _pos, self.file = self.get_object(message, str)
2707
2708
2709class PlaceInQueue(PeerMessage):
2710    """ Peer code: 44 """
2711    """ The peer replies with the upload queue placement of the requested file. """
2712
2713    def __init__(self, conn, filename=None, place=None):
2714        self.conn = conn
2715        self.filename = filename
2716        self.place = place
2717
2718    def make_network_message(self):
2719        msg = bytearray()
2720        msg.extend(self.pack_object(self.filename))
2721        msg.extend(self.pack_object(self.place))
2722
2723        return msg
2724
2725    def parse_network_message(self, message):
2726        pos, self.filename = self.get_object(message, str)
2727        pos, self.place = self.get_object(message, int, pos)
2728
2729
2730class UploadFailed(PlaceholdUpload):
2731    """ Peer code: 46 """
2732    """ This message is sent whenever a file connection of an active upload
2733    closes. Soulseek NS clients can also send this message when a file can
2734    not be read. The recipient either re-queues the upload (download on their
2735    end), or ignores the message if the transfer finished. """
2736
2737
2738class UploadDenied(PeerMessage):
2739    """ Peer code: 50 """
2740    """ This message is sent to reject QueueUpload attempts and previously queued
2741    files. The reason for rejection will appear in the transfer list of the recipient. """
2742
2743    def __init__(self, conn, file=None, reason=None):
2744        self.conn = conn
2745        self.file = file
2746        self.reason = reason
2747
2748    def make_network_message(self):
2749        msg = bytearray()
2750        msg.extend(self.pack_object(self.file))
2751        msg.extend(self.pack_object(self.reason))
2752
2753        return msg
2754
2755    def parse_network_message(self, message):
2756        pos, self.file = self.get_object(message, str)
2757        pos, self.reason = self.get_object(message, str, pos)
2758
2759
2760class PlaceInQueueRequest(QueueUpload):
2761    """ Peer code: 51 """
2762    """ This message is sent when asking for the upload queue placement of a file. """
2763
2764
2765class UploadQueueNotification(PeerMessage):
2766    """ Peer code: 52 """
2767    """ This message is sent to inform a peer about an upload attempt initiated by us. """
2768    """ DEPRECATED, sent by Soulseek NS but not SoulseekQt """
2769
2770    def __init__(self, conn):
2771        self.conn = conn
2772
2773    def make_network_message(self):
2774        return b""
2775
2776    def parse_network_message(self, _message):
2777        return b""
2778
2779
2780class UnknownPeerMessage(PeerMessage):
2781    """ Peer code: 12547 """
2782    """ UNKNOWN """
2783
2784    def __init__(self, conn):
2785        self.conn = conn
2786
2787    def parse_network_message(self, message):
2788        # Empty message
2789        pass
2790
2791
2792"""
2793File Messages
2794"""
2795
2796
2797class FileMessage(SlskMessage):
2798    pass
2799
2800
2801class FileRequest(FileMessage):
2802    """ We sent this to a peer via a 'F' connection to tell them that we want to
2803    start uploading a file. The token is the same as the one previously included
2804    in the TransferRequest message. """
2805
2806    def __init__(self, conn, req=None):
2807        self.conn = conn
2808        self.req = req
2809
2810    def make_network_message(self):
2811        msg = self.pack_object(self.req)
2812        return msg
2813
2814    def parse_network_message(self, message):
2815        _pos, self.req = self.get_object(message, int)
2816
2817
2818class FileOffset(FileMessage):
2819    """ We send this to the uploading peer at the beginning of a 'F' connection,
2820    to tell them how many bytes of the file we've previously downloaded. If none,
2821    the offset is 0. """
2822
2823    def __init__(self, conn, filesize=None, offset=None):
2824        self.conn = conn
2825        self.filesize = filesize
2826        self.offset = offset
2827
2828    def make_network_message(self):
2829        msg = self.pack_object(self.offset, unsignedlonglong=True)
2830        return msg
2831
2832    def parse_network_message(self, message):
2833        _pos, self.offset = self.get_object(message, int, getunsignedlonglong=True)
2834
2835
2836"""
2837Distributed Messages
2838"""
2839
2840
2841class DistribMessage(SlskMessage):
2842    pass
2843
2844
2845class DistribRequest(InternalMessage):
2846    """ Used to identify a connection attempt to a distributed parent. """
2847
2848
2849class DistribAlive(DistribMessage):
2850    """ Distrib code: 0 """
2851
2852    def __init__(self, conn):
2853        self.conn = conn
2854
2855    def make_network_message(self):
2856        return b""
2857
2858    def parse_network_message(self, message):
2859        # Empty message
2860        pass
2861
2862
2863class DistribSearch(DistribMessage):
2864    """ Distrib code: 3 """
2865    """ Search request that arrives through the distributed network.
2866    We transmit the search request to our child peers. """
2867
2868    __slots__ = ("unknown", "conn", "user", "searchid", "searchterm")
2869
2870    def __init__(self, conn):
2871        self.conn = conn
2872        self.unknown = None
2873        self.user = None
2874        self.searchid = None
2875        self.searchterm = None
2876
2877    def parse_network_message(self, message):
2878        try:
2879            self._parse_network_message(message)
2880
2881        except Exception as error:
2882            log.add("Exception during parsing %(area)s: %(exception)s",
2883                    {'area': 'DistribSearch', 'exception': error})
2884
2885    def _parse_network_message(self, message):
2886        pos, self.unknown = self.get_object(message, int)
2887        pos, self.user = self.get_object(message, str, pos)
2888        pos, self.searchid = self.get_object(message, int, pos)
2889        pos, self.searchterm = self.get_object(message, str, pos)
2890
2891
2892class DistribBranchLevel(DistribMessage):
2893    """ Distrib code: 4 """
2894    """ We tell our distributed children what our position is in our branch (xth
2895    generation) on the distributed network. """
2896
2897    def __init__(self, conn, value=None):
2898        self.conn = conn
2899        self.value = value
2900
2901    def make_network_message(self):
2902        msg = bytearray()
2903        msg.extend(self.pack_object(self.value, signedint=True))
2904
2905        return msg
2906
2907    def parse_network_message(self, message):
2908        _pos, self.value = self.get_object(message, int, getsignedint=True)
2909
2910
2911class DistribBranchRoot(DistribMessage):
2912    """ Distrib code: 5 """
2913    """ We tell our distributed children the username of the root of the branch
2914    we’re in on the distributed network. """
2915
2916    def __init__(self, conn, user=None):
2917        self.conn = conn
2918        self.user = user
2919
2920    def make_network_message(self):
2921        msg = bytearray()
2922        msg.extend(self.pack_object(self.user))
2923
2924        return msg
2925
2926    def parse_network_message(self, message):
2927        _pos, self.user = self.get_object(message, str)
2928
2929
2930class DistribChildDepth(DistribMessage):
2931    """ Distrib code: 7 """
2932    """ We tell our distributed parent the maximum number of generation of children
2933    we have on the distributed network. """
2934    """ DEPRECATED, sent by Soulseek NS but not SoulseekQt """
2935
2936    def __init__(self, conn, value=None):
2937        self.conn = conn
2938        self.value = value
2939
2940    def make_network_message(self):
2941        msg = bytearray()
2942        msg.extend(self.pack_object(self.value))
2943
2944        return msg
2945
2946    def parse_network_message(self, message):
2947        _pos, self.value = self.get_object(message, int)
2948
2949
2950class DistribEmbeddedMessage(DistribMessage):
2951    """ Distrib code: 93 """
2952    """ A branch root sends us an embedded distributed message. The only type
2953    of distributed message sent at present is DistribSearch (distributed code 3).
2954    We unpack the distributed message and distribute it to our child peers. """
2955
2956    __slots__ = ("conn", "distrib_code", "distrib_message")
2957
2958    def __init__(self, conn, distrib_code=None, distrib_message=None):
2959        self.conn = conn
2960        self.distrib_code = distrib_code
2961        self.distrib_message = distrib_message
2962
2963    def make_network_message(self):
2964        msg = bytearray()
2965        msg.extend(bytes([self.distrib_code]))
2966        msg.extend(self.distrib_message)
2967
2968        return msg
2969
2970    def parse_network_message(self, message):
2971        self.distrib_code = message[3]
2972        self.distrib_message = message[4:]
2973