正在显示
8 个修改的文件
包含
463 行增加
和
7 行删除
| 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
| 2 | import sys, os | 2 | import sys, os |
| 3 | +import fbchat._mqtt | ||
| 3 | 4 | ||
| 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 5 | sys.path.append(BASE_DIR) | 6 | sys.path.append(BASE_DIR) |
| @@ -7,6 +8,17 @@ sys.path.append(BASE_DIR) | @@ -7,6 +8,17 @@ sys.path.append(BASE_DIR) | ||
| 7 | from core import core | 8 | from core import core |
| 8 | from conf import log_settings | 9 | from conf import log_settings |
| 9 | 10 | ||
| 11 | + | ||
| 12 | +def reload_mqtt(): | ||
| 13 | + '''替换_mqtt模块,增加Proxy支持''' | ||
| 14 | + import importlib.machinery, importlib.util | ||
| 15 | + loader = importlib.machinery.SourceFileLoader('fbchat._mqtt', 'lib/_mqtt.py') | ||
| 16 | + spec = importlib.util.spec_from_loader(loader.name, loader) | ||
| 17 | + fbchat._mqtt = importlib.util.module_from_spec(spec) | ||
| 18 | + loader.exec_module(fbchat._mqtt) | ||
| 19 | + | ||
| 20 | + | ||
| 10 | if __name__ == '__main__': | 21 | if __name__ == '__main__': |
| 11 | # log_settings.load_logging_cfg().info("Running") | 22 | # log_settings.load_logging_cfg().info("Running") |
| 23 | + reload_mqtt() | ||
| 12 | core.run() | 24 | core.run() |
| @@ -16,6 +16,11 @@ from core.monitor import Monitor | @@ -16,6 +16,11 @@ from core.monitor import Monitor | ||
| 16 | from lib import control_server | 16 | from lib import control_server |
| 17 | from lib.socket_ import MessageSocketClient | 17 | from lib.socket_ import MessageSocketClient |
| 18 | 18 | ||
| 19 | +try: | ||
| 20 | + from lib.message_queue import MessageQueue as Queue | ||
| 21 | +except: | ||
| 22 | + Queue = None | ||
| 23 | + | ||
| 19 | monitor = Monitor() | 24 | monitor = Monitor() |
| 20 | monitor.version = settings.get_version() | 25 | monitor.version = settings.get_version() |
| 21 | 26 | ||
| @@ -94,4 +99,9 @@ def run(): | @@ -94,4 +99,9 @@ def run(): | ||
| 94 | sched.start() | 99 | sched.start() |
| 95 | # init schedule end | 100 | # init schedule end |
| 96 | 101 | ||
| 102 | + # message queue start | ||
| 103 | + if Queue: | ||
| 104 | + monitor.queue = Queue(monitor._name, monitor.execute).start() | ||
| 105 | + # message queue end | ||
| 106 | + | ||
| 97 | ioloop.IOLoop.instance().start() | 107 | ioloop.IOLoop.instance().start() |
| @@ -41,6 +41,7 @@ class Monitor(callback.CallBack): | @@ -41,6 +41,7 @@ class Monitor(callback.CallBack): | ||
| 41 | self.version = None | 41 | self.version = None |
| 42 | self.executor = ThreadPoolExecutor(50, 'task_thread') | 42 | self.executor = ThreadPoolExecutor(50, 'task_thread') |
| 43 | self.init_config = {} | 43 | self.init_config = {} |
| 44 | + self.queue = None | ||
| 44 | 45 | ||
| 45 | def bind(self, socket): | 46 | def bind(self, socket): |
| 46 | self._socket = socket | 47 | self._socket = socket |
| @@ -147,6 +148,8 @@ class Monitor(callback.CallBack): | @@ -147,6 +148,8 @@ class Monitor(callback.CallBack): | ||
| 147 | if hasattr(client, 'uid') and client.uid: | 148 | if hasattr(client, 'uid') and client.uid: |
| 148 | data['fbid'] = client.uid | 149 | data['fbid'] = client.uid |
| 149 | payload = add_type("notify", data) | 150 | payload = add_type("notify", data) |
| 151 | + | ||
| 152 | + if self.queue: self.queue.publish_msg(data) | ||
| 150 | self._socket.send(payload) | 153 | self._socket.send(payload) |
| 151 | 154 | ||
| 152 | def _task_(self, type_, client, taskid: int, code, msg: dict = None): | 155 | def _task_(self, type_, client, taskid: int, code, msg: dict = None): |
lib/_mqtt.py
0 → 100644
| 1 | +import re | ||
| 2 | + | ||
| 3 | +import attr | ||
| 4 | +import random | ||
| 5 | +import paho.mqtt.client | ||
| 6 | +import socks | ||
| 7 | +from fbchat._core import log | ||
| 8 | +from fbchat import _util, _exception, _graphql | ||
| 9 | + | ||
| 10 | +import ssl | ||
| 11 | + | ||
| 12 | +ssl._create_default_https_context = ssl._create_unverified_context | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +def generate_session_id(): | ||
| 16 | + """Generate a random session ID between 1 and 9007199254740991.""" | ||
| 17 | + return random.randint(1, 2 ** 53) | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +@attr.s(slots=True) | ||
| 21 | +class Mqtt(object): | ||
| 22 | + _state = attr.ib() | ||
| 23 | + _mqtt = attr.ib() | ||
| 24 | + _on_message = attr.ib() | ||
| 25 | + _chat_on = attr.ib() | ||
| 26 | + _foreground = attr.ib() | ||
| 27 | + _sequence_id = attr.ib() | ||
| 28 | + _sync_token = attr.ib(None) | ||
| 29 | + | ||
| 30 | + _HOST = "edge-chat.facebook.com" | ||
| 31 | + | ||
| 32 | + @classmethod | ||
| 33 | + def connect(cls, state, on_message, chat_on, foreground): | ||
| 34 | + mqtt = paho.mqtt.client.Client( | ||
| 35 | + client_id="mqttwsclient", | ||
| 36 | + clean_session=True, | ||
| 37 | + protocol=paho.mqtt.client.MQTTv31, | ||
| 38 | + transport="websockets", | ||
| 39 | + ) | ||
| 40 | + mqtt.enable_logger() | ||
| 41 | + | ||
| 42 | + if state._session.proxies: | ||
| 43 | + proxy = state._session.params.get('sock') | ||
| 44 | + mqtt.proxy_set(**proxy) | ||
| 45 | + # mqtt.max_inflight_messages_set(20) # The rest will get queued | ||
| 46 | + # mqtt.max_queued_messages_set(0) # Unlimited messages can be queued | ||
| 47 | + # mqtt.message_retry_set(20) # Retry sending for at least 20 seconds | ||
| 48 | + # mqtt.reconnect_delay_set(min_delay=1, max_delay=120) | ||
| 49 | + # TODO: Is region (lla | atn | odn | others?) important? | ||
| 50 | + mqtt.tls_set() | ||
| 51 | + | ||
| 52 | + self = cls( | ||
| 53 | + state=state, | ||
| 54 | + mqtt=mqtt, | ||
| 55 | + on_message=on_message, | ||
| 56 | + chat_on=chat_on, | ||
| 57 | + foreground=foreground, | ||
| 58 | + sequence_id=cls._fetch_sequence_id(state), | ||
| 59 | + ) | ||
| 60 | + | ||
| 61 | + # Configure callbacks | ||
| 62 | + mqtt.on_message = self._on_message_handler | ||
| 63 | + mqtt.on_connect = self._on_connect_handler | ||
| 64 | + | ||
| 65 | + self._configure_connect_options() | ||
| 66 | + | ||
| 67 | + # Attempt to connect | ||
| 68 | + try: | ||
| 69 | + rc = mqtt.connect(self._HOST, 443, keepalive=10) | ||
| 70 | + except ( | ||
| 71 | + # Taken from .loop_forever | ||
| 72 | + paho.mqtt.client.socket.error, | ||
| 73 | + OSError, | ||
| 74 | + paho.mqtt.client.WebsocketConnectionError, | ||
| 75 | + ) as e: | ||
| 76 | + raise _exception.FBchatException("MQTT connection failed") | ||
| 77 | + | ||
| 78 | + # Raise error if connecting failed | ||
| 79 | + if rc != paho.mqtt.client.MQTT_ERR_SUCCESS: | ||
| 80 | + err = paho.mqtt.client.error_string(rc) | ||
| 81 | + raise _exception.FBchatException("MQTT connection failed: {}".format(err)) | ||
| 82 | + | ||
| 83 | + return self | ||
| 84 | + | ||
| 85 | + def _on_message_handler(self, client, userdata, message): | ||
| 86 | + # Parse payload JSON | ||
| 87 | + try: | ||
| 88 | + j = _util.parse_json(message.payload.decode("utf-8")) | ||
| 89 | + except (_exception.FBchatFacebookError, UnicodeDecodeError): | ||
| 90 | + log.exception("Failed parsing MQTT data on %s as JSON", message.topic) | ||
| 91 | + return | ||
| 92 | + | ||
| 93 | + if message.topic == "/t_ms": | ||
| 94 | + # Update sync_token when received | ||
| 95 | + # This is received in the first message after we've created a messenger | ||
| 96 | + # sync queue. | ||
| 97 | + if "syncToken" in j and "firstDeltaSeqId" in j: | ||
| 98 | + self._sync_token = j["syncToken"] | ||
| 99 | + self._sequence_id = j["firstDeltaSeqId"] | ||
| 100 | + | ||
| 101 | + # Update last sequence id when received | ||
| 102 | + if "lastIssuedSeqId" in j: | ||
| 103 | + self._sequence_id = j["lastIssuedSeqId"] | ||
| 104 | + | ||
| 105 | + if "errorCode" in j: | ||
| 106 | + # Known types: ERROR_QUEUE_OVERFLOW | ERROR_QUEUE_NOT_FOUND | ||
| 107 | + # 'F\xfa\x84\x8c\x85\xf8\xbc-\x88 FB_PAGES_INSUFFICIENT_PERMISSION\x00' | ||
| 108 | + log.error("MQTT error code %s received", j["errorCode"]) | ||
| 109 | + # TODO: Consider resetting the sync_token and sequence ID here? | ||
| 110 | + | ||
| 111 | + log.debug("MQTT payload: %s, %s", message.topic, j) | ||
| 112 | + | ||
| 113 | + # Call the external callback | ||
| 114 | + self._on_message(message.topic, j) | ||
| 115 | + | ||
| 116 | + @staticmethod | ||
| 117 | + def _fetch_sequence_id(state): | ||
| 118 | + """Fetch sequence ID.""" | ||
| 119 | + params = { | ||
| 120 | + "limit": 1, | ||
| 121 | + "tags": ["INBOX"], | ||
| 122 | + "before": None, | ||
| 123 | + "includeDeliveryReceipts": False, | ||
| 124 | + "includeSeqID": True, | ||
| 125 | + } | ||
| 126 | + log.debug("Fetching MQTT sequence ID") | ||
| 127 | + # Same request as in `Client.fetchThreadList` | ||
| 128 | + (j,) = state._graphql_requests(_graphql.from_doc_id("1349387578499440", params)) | ||
| 129 | + try: | ||
| 130 | + return int(j["viewer"]["message_threads"]["sync_sequence_id"]) | ||
| 131 | + except (KeyError, ValueError): | ||
| 132 | + # TODO: Proper exceptions | ||
| 133 | + raise | ||
| 134 | + | ||
| 135 | + def _on_connect_handler(self, client, userdata, flags, rc): | ||
| 136 | + if rc == 21: | ||
| 137 | + raise _exception.FBchatException( | ||
| 138 | + "Failed connecting. Maybe your cookies are wrong?" | ||
| 139 | + ) | ||
| 140 | + if rc != 0: | ||
| 141 | + return # Don't try to send publish if the connection failed | ||
| 142 | + | ||
| 143 | + # configure receiving messages. | ||
| 144 | + payload = { | ||
| 145 | + "sync_api_version": 10, | ||
| 146 | + "max_deltas_able_to_process": 1000, | ||
| 147 | + "delta_batch_size": 500, | ||
| 148 | + "encoding": "JSON", | ||
| 149 | + "entity_fbid": self._state.user_id, | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + # If we don't have a sync_token, create a new messenger queue | ||
| 153 | + # This is done so that across reconnects, if we've received a sync token, we | ||
| 154 | + # SHOULD receive a piece of data in /t_ms exactly once! | ||
| 155 | + if self._sync_token is None: | ||
| 156 | + topic = "/messenger_sync_create_queue" | ||
| 157 | + payload["initial_titan_sequence_id"] = str(self._sequence_id) | ||
| 158 | + payload["device_params"] = None | ||
| 159 | + else: | ||
| 160 | + topic = "/messenger_sync_get_diffs" | ||
| 161 | + payload["last_seq_id"] = str(self._sequence_id) | ||
| 162 | + payload["sync_token"] = self._sync_token | ||
| 163 | + | ||
| 164 | + self._mqtt.publish(topic, _util.json_minimal(payload), qos=1) | ||
| 165 | + | ||
| 166 | + def _configure_connect_options(self): | ||
| 167 | + # Generate a new session ID on each reconnect | ||
| 168 | + session_id = generate_session_id() | ||
| 169 | + | ||
| 170 | + topics = [ | ||
| 171 | + # Things that happen in chats (e.g. messages) | ||
| 172 | + "/t_ms", | ||
| 173 | + # Group typing notifications | ||
| 174 | + "/thread_typing", | ||
| 175 | + # Private chat typing notifications | ||
| 176 | + "/orca_typing_notifications", | ||
| 177 | + # Active notifications | ||
| 178 | + "/orca_presence", | ||
| 179 | + # Other notifications not related to chats (e.g. friend requests) | ||
| 180 | + "/legacy_web", | ||
| 181 | + # Facebook's continuous error reporting/logging? | ||
| 182 | + "/br_sr", | ||
| 183 | + # Response to /br_sr | ||
| 184 | + "/sr_res", | ||
| 185 | + # TODO: Investigate the response from this! (A bunch of binary data) | ||
| 186 | + # "/t_p", | ||
| 187 | + # TODO: Find out what this does! | ||
| 188 | + "/webrtc", | ||
| 189 | + # TODO: Find out what this does! | ||
| 190 | + "/onevc", | ||
| 191 | + # TODO: Find out what this does! | ||
| 192 | + "/notify_disconnect", | ||
| 193 | + # Old, no longer active topics | ||
| 194 | + # These are here just in case something interesting pops up | ||
| 195 | + "/inbox", | ||
| 196 | + "/mercury", | ||
| 197 | + "/messaging_events", | ||
| 198 | + "/orca_message_notifications", | ||
| 199 | + "/pp", | ||
| 200 | + "/t_rtc", | ||
| 201 | + "/webrtc_response", | ||
| 202 | + ] | ||
| 203 | + | ||
| 204 | + username = { | ||
| 205 | + # The user ID | ||
| 206 | + "u": self._state.user_id, | ||
| 207 | + # Session ID | ||
| 208 | + "s": session_id, | ||
| 209 | + # Active status setting | ||
| 210 | + "chat_on": self._chat_on, | ||
| 211 | + # foreground_state - Whether the window is focused | ||
| 212 | + "fg": self._foreground, | ||
| 213 | + # Can be any random ID | ||
| 214 | + "d": self._state._client_id, | ||
| 215 | + # Application ID, taken from facebook.com | ||
| 216 | + "aid": 219994525426954, | ||
| 217 | + # MQTT extension by FB, allows making a SUBSCRIBE while CONNECTing | ||
| 218 | + "st": topics, | ||
| 219 | + # MQTT extension by FB, allows making a PUBLISH while CONNECTing | ||
| 220 | + # Using this is more efficient, but the same can be acheived with: | ||
| 221 | + # def on_connect(*args): | ||
| 222 | + # mqtt.publish(topic, payload, qos=1) | ||
| 223 | + # mqtt.on_connect = on_connect | ||
| 224 | + # TODO: For some reason this doesn't work! | ||
| 225 | + "pm": [ | ||
| 226 | + # { | ||
| 227 | + # "topic": topic, | ||
| 228 | + # "payload": payload, | ||
| 229 | + # "qos": 1, | ||
| 230 | + # "messageId": 65536, | ||
| 231 | + # } | ||
| 232 | + ], | ||
| 233 | + # Unknown parameters | ||
| 234 | + "cp": 3, | ||
| 235 | + "ecp": 10, | ||
| 236 | + "ct": "websocket", | ||
| 237 | + "mqtt_sid": "", | ||
| 238 | + "dc": "", | ||
| 239 | + "no_auto_fg": True, | ||
| 240 | + "gas": None, | ||
| 241 | + "pack": [], | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + # TODO: Make this thread safe | ||
| 245 | + self._mqtt.username_pw_set(_util.json_minimal(username)) | ||
| 246 | + | ||
| 247 | + headers = { | ||
| 248 | + # TODO: Make this access thread safe | ||
| 249 | + "Cookie": _util.get_cookie_header( | ||
| 250 | + self._state._session, "https://edge-chat.facebook.com/chat" | ||
| 251 | + ), | ||
| 252 | + "User-Agent": self._state._session.headers["User-Agent"], | ||
| 253 | + "Origin": "https://www.facebook.com", | ||
| 254 | + "Host": self._HOST, | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + self._mqtt.ws_set_options( | ||
| 258 | + path="/chat?sid={}".format(session_id), headers=headers | ||
| 259 | + ) | ||
| 260 | + | ||
| 261 | + def loop_once(self, on_error=None): | ||
| 262 | + """Run the listening loop once. | ||
| 263 | + | ||
| 264 | + Returns whether to keep listening or not. | ||
| 265 | + """ | ||
| 266 | + rc = self._mqtt.loop(timeout=1.0) | ||
| 267 | + | ||
| 268 | + # If disconnect() has been called | ||
| 269 | + if self._mqtt._state == paho.mqtt.client.mqtt_cs_disconnecting: | ||
| 270 | + return False # Stop listening | ||
| 271 | + | ||
| 272 | + if rc != paho.mqtt.client.MQTT_ERR_SUCCESS: | ||
| 273 | + # If known/expected error | ||
| 274 | + if rc == paho.mqtt.client.MQTT_ERR_CONN_LOST: | ||
| 275 | + log.warning("Connection lost, retrying") | ||
| 276 | + elif rc == paho.mqtt.client.MQTT_ERR_NOMEM: | ||
| 277 | + # This error is wrongly classified | ||
| 278 | + # See https://github.com/eclipse/paho.mqtt.python/issues/340 | ||
| 279 | + log.warning("Connection error, retrying") | ||
| 280 | + else: | ||
| 281 | + err = paho.mqtt.client.error_string(rc) | ||
| 282 | + log.error("MQTT Error: %s", err) | ||
| 283 | + # For backwards compatibility | ||
| 284 | + if on_error: | ||
| 285 | + on_error(_exception.FBchatException("MQTT Error {}".format(err))) | ||
| 286 | + | ||
| 287 | + # Wait before reconnecting | ||
| 288 | + self._mqtt._reconnect_wait() | ||
| 289 | + | ||
| 290 | + # Try reconnecting | ||
| 291 | + self._configure_connect_options() | ||
| 292 | + try: | ||
| 293 | + self._mqtt.reconnect() | ||
| 294 | + except ( | ||
| 295 | + # Taken from .loop_forever | ||
| 296 | + paho.mqtt.client.socket.error, | ||
| 297 | + OSError, | ||
| 298 | + paho.mqtt.client.WebsocketConnectionError, | ||
| 299 | + ) as e: | ||
| 300 | + log.debug("MQTT reconnection failed: %s", e) | ||
| 301 | + | ||
| 302 | + return True # Keep listening | ||
| 303 | + | ||
| 304 | + def disconnect(self): | ||
| 305 | + self._mqtt.disconnect() | ||
| 306 | + | ||
| 307 | + def set_foreground(self, value): | ||
| 308 | + payload = _util.json_minimal({"foreground": value}) | ||
| 309 | + info = self._mqtt.publish("/foreground_state", payload=payload, qos=1) | ||
| 310 | + self._foreground = value | ||
| 311 | + # TODO: We can't wait for this, since the loop is running with .loop_forever() | ||
| 312 | + # info.wait_for_publish() | ||
| 313 | + | ||
| 314 | + def set_chat_on(self, value): | ||
| 315 | + # TODO: Is this the right request to make? | ||
| 316 | + data = {"make_user_available_when_in_foreground": value} | ||
| 317 | + payload = _util.json_minimal(data) | ||
| 318 | + info = self._mqtt.publish("/set_client_settings", payload=payload, qos=1) | ||
| 319 | + self._chat_on = value | ||
| 320 | + # TODO: We can't wait for this, since the loop is running with .loop_forever() | ||
| 321 | + # info.wait_for_publish() | ||
| 322 | + | ||
| 323 | + # def send_additional_contacts(self, additional_contacts): | ||
| 324 | + # payload = _util.json_minimal({"additional_contacts": additional_contacts}) | ||
| 325 | + # info = self._mqtt.publish("/send_additional_contacts", payload=payload, qos=1) | ||
| 326 | + # | ||
| 327 | + # def browser_close(self): | ||
| 328 | + # info = self._mqtt.publish("/browser_close", payload=b"{}", qos=1) |
| @@ -104,7 +104,6 @@ class College(): | @@ -104,7 +104,6 @@ class College(): | ||
| 104 | data["concentration_ids[%d]" % i] = sub[1] | 104 | data["concentration_ids[%d]" % i] = sub[1] |
| 105 | return data | 105 | return data |
| 106 | 106 | ||
| 107 | - | ||
| 108 | @classmethod | 107 | @classmethod |
| 109 | def from_dict(cls, data: dict): | 108 | def from_dict(cls, data: dict): |
| 110 | self = cls() | 109 | self = cls() |
| @@ -176,7 +175,7 @@ def todict(obj, include: list = None): | @@ -176,7 +175,7 @@ def todict(obj, include: list = None): | ||
| 176 | return res | 175 | return res |
| 177 | 176 | ||
| 178 | 177 | ||
| 179 | -def tobase64(obj): | 178 | +def tobase64(obj) -> str: |
| 180 | if isinstance(obj, (dict, list)): | 179 | if isinstance(obj, (dict, list)): |
| 181 | obj = demjson.encode(obj) | 180 | obj = demjson.encode(obj) |
| 182 | if isinstance(obj, str): | 181 | if isinstance(obj, str): |
| @@ -187,6 +186,66 @@ def tobase64(obj): | @@ -187,6 +186,66 @@ def tobase64(obj): | ||
| 187 | raise BaseException("must str,list,dict,bytes") | 186 | raise BaseException("must str,list,dict,bytes") |
| 188 | 187 | ||
| 189 | 188 | ||
| 190 | -def frombase64(base): | 189 | +def frombase64(base) -> dict: |
| 191 | string = base64.b64decode(base).decode() | 190 | string = base64.b64decode(base).decode() |
| 192 | return demjson.decode(string) | 191 | return demjson.decode(string) |
| 192 | + | ||
| 193 | + | ||
| 194 | +def format_proxies(conf: dict, format='requests'): | ||
| 195 | + ''' | ||
| 196 | + e.g. | ||
| 197 | + { | ||
| 198 | + 'type': 'socks5', | ||
| 199 | + 'host': '47.56.152.111', | ||
| 200 | + 'pass': 'nantian888', | ||
| 201 | + 'port': '1080', | ||
| 202 | + 'user': 'ntkj' | ||
| 203 | + } | ||
| 204 | + ''' | ||
| 205 | + type_ = conf.get('type', None) | ||
| 206 | + host = conf.get('host', None) | ||
| 207 | + port = conf.get('port', None) | ||
| 208 | + user = conf.get('user', None) | ||
| 209 | + pass_ = conf.get('pass', None) | ||
| 210 | + | ||
| 211 | + if format == 'requests': | ||
| 212 | + info = [] | ||
| 213 | + info.append(type_) | ||
| 214 | + info.append("://") | ||
| 215 | + p_auth = [] | ||
| 216 | + if user: | ||
| 217 | + p_auth.append(str(user)) | ||
| 218 | + p_auth.append(":") | ||
| 219 | + if pass_: | ||
| 220 | + p_auth.append(str(pass_)) | ||
| 221 | + if p_auth: | ||
| 222 | + info.append(''.join(p_auth)) | ||
| 223 | + info.append('@') | ||
| 224 | + info.append(host) | ||
| 225 | + if port: | ||
| 226 | + info.append(':') | ||
| 227 | + info.append(str(port)) | ||
| 228 | + text = ''.join(info) | ||
| 229 | + return {'http': text, 'https': text} | ||
| 230 | + else: | ||
| 231 | + _type_ = getattr(__import__('socks'), 'PROXY_TYPES').get(type_.upper()) | ||
| 232 | + assert _type_, '代理协议错误,可选socks5,socks4,http' | ||
| 233 | + proxy = { | ||
| 234 | + "proxy_type": _type_, | ||
| 235 | + "proxy_addr": host, | ||
| 236 | + "proxy_port": int(port), | ||
| 237 | + } | ||
| 238 | + if user: | ||
| 239 | + proxy['proxy_username'] = user | ||
| 240 | + if pass_: | ||
| 241 | + proxy['proxy_password'] = pass_ | ||
| 242 | + return proxy | ||
| 243 | + | ||
| 244 | + | ||
| 245 | +if __name__ == '__main__': | ||
| 246 | + conf = {'type': 'http', | ||
| 247 | + 'host': '47.56.152.111', | ||
| 248 | + 'pass': 'nantian888', | ||
| 249 | + 'port': '1080', | ||
| 250 | + 'user': 'ntkj'} | ||
| 251 | + print(format_proxies(conf, '1')) |
| @@ -14,10 +14,12 @@ from enum import Enum | @@ -14,10 +14,12 @@ from enum import Enum | ||
| 14 | 14 | ||
| 15 | import demjson | 15 | import demjson |
| 16 | import furl | 16 | import furl |
| 17 | +import requests | ||
| 17 | from fbchat import Client, ThreadType, Message, Sticker, FBchatUserError, _exception, log, _util | 18 | from fbchat import Client, ThreadType, Message, Sticker, FBchatUserError, _exception, log, _util |
| 18 | from fbchat._state import State, session_factory, is_home | 19 | from fbchat._state import State, session_factory, is_home |
| 19 | 20 | ||
| 20 | from lib import google_map, common | 21 | from lib import google_map, common |
| 22 | +from lib._mqtt import Mqtt | ||
| 21 | from lib.common import WorkPlace, College | 23 | from lib.common import WorkPlace, College |
| 22 | from utils import parse_html, _attachment | 24 | from utils import parse_html, _attachment |
| 23 | 25 | ||
| @@ -25,10 +27,16 @@ from utils import parse_html, _attachment | @@ -25,10 +27,16 @@ from utils import parse_html, _attachment | ||
| 25 | class PCState(State): | 27 | class PCState(State): |
| 26 | 28 | ||
| 27 | @classmethod | 29 | @classmethod |
| 28 | - def login(cls, email, password, on_2fa_callback, user_agent=None): | 30 | + def login(cls, email, password, on_2fa_callback, user_agent=None, proxy=None): |
| 29 | '''换成了PC的登录方式''' | 31 | '''换成了PC的登录方式''' |
| 30 | session = session_factory(user_agent=user_agent) | 32 | session = session_factory(user_agent=user_agent) |
| 31 | 33 | ||
| 34 | + if proxy: | ||
| 35 | + short_proxy = common.format_proxies(proxy, 'requests') | ||
| 36 | + long_proxy = common.format_proxies(proxy, 'sock') | ||
| 37 | + session.params.update({'sock': long_proxy}) | ||
| 38 | + session.proxies.update(short_proxy) | ||
| 39 | + | ||
| 32 | soup = parse_html.find_input_fields_with_pc(session.get("https://www.facebook.com/").text) | 40 | soup = parse_html.find_input_fields_with_pc(session.get("https://www.facebook.com/").text) |
| 33 | data = dict((elem["name"], elem["value"]) | 41 | data = dict((elem["name"], elem["value"]) |
| 34 | for elem in soup | 42 | for elem in soup |
| @@ -71,6 +79,19 @@ class PCState(State): | @@ -71,6 +79,19 @@ class PCState(State): | ||
| 71 | return "Location" in r.headers and is_home(r.headers["Location"]) | 79 | return "Location" in r.headers and is_home(r.headers["Location"]) |
| 72 | 80 | ||
| 73 | @classmethod | 81 | @classmethod |
| 82 | + def from_cookies(cls, cookies, user_agent=None, proxy=None): | ||
| 83 | + session = session_factory(user_agent=user_agent) | ||
| 84 | + session.cookies = requests.cookies.merge_cookies(session.cookies, cookies) | ||
| 85 | + | ||
| 86 | + if proxy: | ||
| 87 | + short_proxy = common.format_proxies(proxy, 'requests') | ||
| 88 | + long_proxy = common.format_proxies(proxy, 'sock') | ||
| 89 | + | ||
| 90 | + session.params.update({'sock': long_proxy}) | ||
| 91 | + session.proxies.update(short_proxy) | ||
| 92 | + return cls.from_session(session=session) | ||
| 93 | + | ||
| 94 | + @classmethod | ||
| 74 | def from_session(cls, session): | 95 | def from_session(cls, session): |
| 75 | 96 | ||
| 76 | def get_user_id(session): | 97 | def get_user_id(session): |
| @@ -81,7 +102,7 @@ class PCState(State): | @@ -81,7 +102,7 @@ class PCState(State): | ||
| 81 | 102 | ||
| 82 | user_id = get_user_id(session) | 103 | user_id = get_user_id(session) |
| 83 | 104 | ||
| 84 | - r = session.get(_util.prefix_url("/"), timeout=10) | 105 | + r = session.get(_util.prefix_url("/"), timeout=30) |
| 85 | 106 | ||
| 86 | b = parse_html.show_home_page(r.text) | 107 | b = parse_html.show_home_page(r.text) |
| 87 | logout_menu = b.find('div', id='logoutMenu') | 108 | logout_menu = b.find('div', id='logoutMenu') |
| @@ -171,6 +192,7 @@ class FacebookClient(Client): | @@ -171,6 +192,7 @@ class FacebookClient(Client): | ||
| 171 | self.email = user_obj.email | 192 | self.email = user_obj.email |
| 172 | self.user_obj = user_obj | 193 | self.user_obj = user_obj |
| 173 | self.extend = None | 194 | self.extend = None |
| 195 | + self.proxy = user_obj.proxy if hasattr(user_obj, 'proxy') else None | ||
| 174 | 196 | ||
| 175 | super().__init__(user_obj.email, user_obj.password, user_obj.user_agent, max_tries, user_obj.format_cookie()) | 197 | super().__init__(user_obj.email, user_obj.password, user_obj.user_agent, max_tries, user_obj.format_cookie()) |
| 176 | 198 | ||
| @@ -179,7 +201,7 @@ class FacebookClient(Client): | @@ -179,7 +201,7 @@ class FacebookClient(Client): | ||
| 179 | 201 | ||
| 180 | def setSession(self, session_cookies, user_agent=None): | 202 | def setSession(self, session_cookies, user_agent=None): |
| 181 | try: | 203 | try: |
| 182 | - self._state = PCState.from_cookies(session_cookies, user_agent=user_agent) | 204 | + self._state = PCState.from_cookies(session_cookies, user_agent=user_agent, proxy=self.proxy) |
| 183 | self._uid = self._state.user_id | 205 | self._uid = self._state.user_id |
| 184 | except Exception as e: | 206 | except Exception as e: |
| 185 | log.exception("Failed loading session") | 207 | log.exception("Failed loading session") |
| @@ -203,6 +225,7 @@ class FacebookClient(Client): | @@ -203,6 +225,7 @@ class FacebookClient(Client): | ||
| 203 | password, | 225 | password, |
| 204 | on_2fa_callback=self.on2FACode, | 226 | on_2fa_callback=self.on2FACode, |
| 205 | user_agent=user_agent, | 227 | user_agent=user_agent, |
| 228 | + proxy=self.proxy | ||
| 206 | ) | 229 | ) |
| 207 | self._uid = self._state.user_id | 230 | self._uid = self._state.user_id |
| 208 | except Exception as err: | 231 | except Exception as err: |
| @@ -1010,3 +1033,21 @@ class FacebookClient(Client): | @@ -1010,3 +1033,21 @@ class FacebookClient(Client): | ||
| 1010 | 'ext_data': next_data | 1033 | 'ext_data': next_data |
| 1011 | } | 1034 | } |
| 1012 | return response | 1035 | return response |
| 1036 | + | ||
| 1037 | + def startListening(self): | ||
| 1038 | + if not self._mqtt: | ||
| 1039 | + self._mqtt = Mqtt.connect( | ||
| 1040 | + state=self._state, | ||
| 1041 | + on_message=self._parse_message, | ||
| 1042 | + chat_on=self._markAlive, | ||
| 1043 | + foreground=True, | ||
| 1044 | + ) | ||
| 1045 | + self.onQprimer(ts=int(time.time() * 1000), msg=None) | ||
| 1046 | + self.listening = True | ||
| 1047 | + | ||
| 1048 | + def securityDevice(self): | ||
| 1049 | + data = {'av': self.uid, 'fb_api_caller_class': 'RelayModern', | ||
| 1050 | + 'fb_api_req_friendly_name': 'SecuritySettingsSessionGroupRefetchQuery', | ||
| 1051 | + 'variables': '{"session_count":20}', 'doc_id': '2549907381730805'} | ||
| 1052 | + res = self._post('https://www.facebook.com/api/graphql/', data) | ||
| 1053 | + return res['data']['security_settings']['sessions'] |
| @@ -73,6 +73,8 @@ class UserList(Base): | @@ -73,6 +73,8 @@ class UserList(Base): | ||
| 73 | fbid = Column(String(20), index=True) | 73 | fbid = Column(String(20), index=True) |
| 74 | status = Column(Integer, default=0, nullable=False, index=True) | 74 | status = Column(Integer, default=0, nullable=False, index=True) |
| 75 | 75 | ||
| 76 | + # proxy = Column(String(256)) | ||
| 77 | + | ||
| 76 | def __repr__(self): | 78 | def __repr__(self): |
| 77 | return "User(id={}, email={}, password={}, cookie={}, fbid={}, status={})" \ | 79 | return "User(id={}, email={}, password={}, cookie={}, fbid={}, status={})" \ |
| 78 | .format(self.id, self.email, self.password, len(self.cookie) if self.cookie else None, self.fbid, | 80 | .format(self.id, self.email, self.password, len(self.cookie) if self.cookie else None, self.fbid, |
-
请 注册 或 登录 后发表评论