summaryrefslogtreecommitdiff
path: root/game/python-extra/discord_rpc/connection/rpc.py
blob: ad110075260f454aadaa83212503cb7f439aacc4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
from __future__ import absolute_import
import logging
import json
from ..codes import errorcodes
from ..codes import opcodes
from ..codes import statecodes
from ..util.utils import is_callable, json2dict, range
from .ipc import BaseConnection


_RPC_VERSION = 1


class RpcConnection(object):
    _connection = None
    _state = statecodes.Disconnected
    _app_id = None
    _last_err_code = 0
    _last_err_msg = ''
    _pipe_no = 0
    _on_connect = None
    _on_disconnect = None

    def __init__(self, app_id, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO):
        self._connection = BaseConnection(log=log, logger=logger, log_file=log_file, log_level=log_level)
        self._app_id = str(app_id)
        if pipe_no in range(0, 10):
            self._pipe_no = pipe_no

    def open(self):
        if self.state == statecodes.Connected:
            self.log('debug', 'Already connected; no need to open.')
            return

        if self.state == statecodes.Disconnected:
            self.connection.open(pipe_no=self._pipe_no)
            if not self.connection.is_open:
                self.log('warning', 'Failed to open IPC connection.')
                return

        if self.state == statecodes.SentHandshake:
            did_read, data = self.read()
            if did_read:
                cmd = data.get('cmd', None)
                evt = data.get('evt', None)
                if all(x is not None for x in (cmd, evt)) and cmd == 'DISPATCH' and evt == 'READY':
                    self.state = statecodes.Connected
                    if self.on_connect is not None:
                        self.on_connect(data)
                    self.log('info', 'IPC connected successfully.')
        else:
            data = {'v': _RPC_VERSION, 'client_id': self.app_id}
            if self.connection.write(json.dumps(data), opcodes.Handshake):
                self.state = statecodes.SentHandshake
                self.log('debug', 'IPC connection sent handshake.')
            else:
                self.log('warning', 'IPC failed to send handshake.')
                self.close()

    def close(self):
        if self.on_disconnect is not None and self.state in (statecodes.Connected, statecodes.SentHandshake):
            self.on_disconnect(self._last_err_code, self._last_err_msg)
            self.log('debug', 'Attempting to close IPC connection.')
        if self.connection is not None:
            self.connection.close()
        else:
            self.log('warning', 'Called close without a connection!')
        self.state = statecodes.Disconnected

    def write(self, data):
        if isinstance(data, dict):
            data = json.dumps(data)
        if not self.connection.write(data, opcodes.Frame):
            self.log('warning', 'Failed to write frame to IPC connection.')
            self.close()
            return False
        return True

    def read(self):
        if self.state not in (statecodes.Connected, statecodes.SentHandshake):
            self.log('debug', 'We aren\'t connected, therefore we cannot read data yet.')
            return False
        while True:
            did_read, opcode, data = self.connection.read()
            self.log('debug', 'ipc.read(): read: {}, Opcode: {}, data: {}'.format(did_read, opcode, data))
            if not did_read:
                err_reason = data[0]
                if (err_reason == errorcodes.PipeClosed and not self.connection.is_open) \
                        or err_reason == errorcodes.ReadCorrupt:
                    self._last_err_code = err_reason
                    self._last_err_msg = data[1]
                    self.log('debug', 'Failed to read; Connection closed. {}'.format(data))
                    self.close()
                return False, None
            if opcode == opcodes.Close:
                data = json2dict(data)
                self._last_err_code = data.get('code', -1)
                self._last_err_msg = data.get('message', '')
                self.log('debug', 'Opcode == Close. Closing connection.')
                self.close()
                return False, None
            elif opcode == opcodes.Frame:
                data = json2dict(data)
                self.log('debug', 'Successful read: {}'.format(data))
                return True, data
            elif opcode == opcodes.Ping:
                if not self.connection.write('', opcodes.Pong):
                    self.log('warning', 'Failed to send Pong message.')
                    self.close()
            elif opcode == opcodes.Pong:
                # Discord does nothing here
                pass
            else:
                # something bad happened
                self._last_err_code = errorcodes.ReadCorrupt
                self._last_err_msg = 'Bad IPC frame.'
                self.log('warning', 'Got a bad frame from IPC connection.')
                self.close()
                return False, None

    def destroy(self):
        self.log('info', 'Destroying RPC connection.')
        self.close()
        self.connection.destroy()
        self._connection = None

    def log(self, *args):
        if self._connection is not None:
            self._connection.log(*args)

    @property
    def connection(self):
        return self._connection

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, state):
        if isinstance(state, int) and state in (statecodes.Connected, statecodes.SentHandshake,
                                                statecodes.Disconnected, statecodes.AwaitingResponse):
            self._state = state
        else:
            self.log('warning', 'Invalid state number!')

    @property
    def app_id(self):
        return self._app_id

    @property
    def is_open(self):
        return self.state == statecodes.Connected

    @property
    def on_connect(self):
        return self._on_connect

    @on_connect.setter
    def on_connect(self, callback):
        if callback is None or is_callable(callback):
            self._on_connect = callback
        else:
            self.log('warning', 'on_connect must be callable/None!')

    @property
    def on_disconnect(self):
        return self._on_disconnect

    @on_disconnect.setter
    def on_disconnect(self, callback):
        if callback is None or is_callable(callback):
            self._on_disconnect = callback
        else:
            self.log('warning', 'on_disconnect must be callable/None!')