From 293b770549597be2099782ab6605a298dbfe5cbf Mon Sep 17 00:00:00 2001 From: niklas Date: Mon, 24 Aug 2020 12:16:53 +0200 Subject: added stuff from pysignald, gonna take over develoment (for us) --- pysignald/__init__.py | 4 + pysignald/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 203 bytes pysignald/__pycache__/main.cpython-37.pyc | Bin 0 -> 6450 bytes pysignald/__pycache__/types.cpython-37.pyc | Bin 0 -> 935 bytes pysignald/main.py | 212 ++++++++++++++++++++++++++ pysignald/types.py | 24 +++ 6 files changed, 240 insertions(+) create mode 100644 pysignald/__init__.py create mode 100644 pysignald/__pycache__/__init__.cpython-37.pyc create mode 100644 pysignald/__pycache__/main.cpython-37.pyc create mode 100644 pysignald/__pycache__/types.cpython-37.pyc create mode 100644 pysignald/main.py create mode 100644 pysignald/types.py diff --git a/pysignald/__init__.py b/pysignald/__init__.py new file mode 100644 index 0000000..959fbf5 --- /dev/null +++ b/pysignald/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +__version__ = "0.0.6" + +from .main import Signal diff --git a/pysignald/__pycache__/__init__.cpython-37.pyc b/pysignald/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..dc56638 Binary files /dev/null and b/pysignald/__pycache__/__init__.cpython-37.pyc differ diff --git a/pysignald/__pycache__/main.cpython-37.pyc b/pysignald/__pycache__/main.cpython-37.pyc new file mode 100644 index 0000000..3325cc3 Binary files /dev/null and b/pysignald/__pycache__/main.cpython-37.pyc differ diff --git a/pysignald/__pycache__/types.cpython-37.pyc b/pysignald/__pycache__/types.cpython-37.pyc new file mode 100644 index 0000000..20b838c Binary files /dev/null and b/pysignald/__pycache__/types.cpython-37.pyc differ diff --git a/pysignald/main.py b/pysignald/main.py new file mode 100644 index 0000000..cfaa79c --- /dev/null +++ b/pysignald/main.py @@ -0,0 +1,212 @@ +import json +import random +import re +import socket +from typing import Iterator, List # noqa + +from .types import Attachment, Message + +# We'll need to know the compiled RE object later. +RE_TYPE = type(re.compile("")) + + +def readlines(s: socket.socket) -> Iterator[bytes]: + "Read a socket, line by line." + buf = [] # type: List[bytes] + while True: + char = s.recv(1) + if not char: + raise ConnectionResetError("connection was reset") + + if char == b"\n": + yield b"".join(buf) + buf = [] + else: + buf.append(char) + + +class Signal: + def __init__(self, username, socket_path="/var/run/signald/signald.sock"): + self.username = username + self.socket_path = socket_path + self._chat_handlers = [] + + def _get_id(self): + "Generate a random ID." + return "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10)) + + def _get_socket(self) -> socket.socket: + "Create a socket, connect to the server and return it." + + # Support TCP sockets on the sly. + if isinstance(self.socket_path, tuple): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + else: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(self.socket_path) + return s + + def _send_command(self, payload: dict, block: bool = False): + s = self._get_socket() + msg_id = self._get_id() + payload["id"] = msg_id + s.recv(1024) # Flush the buffer. + s.send(json.dumps(payload).encode("utf8") + b"\n") + + if not block: + return + + response = s.recv(4 * 1024) + for line in response.split(b"\n"): + if msg_id.encode("utf8") not in line: + continue + + data = json.loads(line) + + if data.get("id") != msg_id: + continue + + if data["type"] == "unexpected_error": + raise ValueError("unexpected error occurred: " + str(line)) + + def register(self, voice=False): + """ + Register the given number. + + voice: Whether to receive a voice call or an SMS for verification. + """ + payload = {"type": "register", "username": self.username, "voice": voice} + self._send_command(payload) + + def verify(self, code: str): + """ + Verify the given number by entering the code you received. + + code: The code Signal sent you. + """ + payload = {"type": "verify", "username": self.username, "code": code} + self._send_command(payload) + + def receive_messages(self) -> Iterator[Message]: + "Keep returning received messages." + s = self._get_socket() + s.send(json.dumps({"type": "subscribe", "username": self.username}).encode("utf8") + b"\n") + + for line in readlines(s): + try: + message = json.loads(line.decode()) + except json.JSONDecodeError: + print("Invalid JSON") + + #print("type: " + message.get("type")) + if message.get("type") != "message": #or ( + #not message["data"]["isReceipt"] and message["data"].get("dataMessage") is None + #): + # If the message type isn't "message", or if it's a weird message whose + # purpose I don't know, return. I think the weird message is a typing + # notification. + continue + + message = message["data"] + data_message = message.get("dataMessage", {}) + + yield Message( + username=message["username"], + source=message["source"], + text=data_message.get("body"), + source_device=message["sourceDevice"], + timestamp=data_message.get("timestamp"), + timestamp_iso=message["timestampISO"], + expiration_secs=data_message.get("expiresInSeconds"), +# is_receipt=message["isReceipt"], + group_info=data_message.get("group", {}), + attachments=[ + Attachment( + content_type=attachment["contentType"], + id=attachment["id"], + size=attachment["size"], + stored_filename=attachment["storedFilename"], + ) + for attachment in data_message.get("attachments", []) + ], + ) + + def send_message(self, recipient: str, text: str, block: bool = True) -> None: + """ + Send a message. + + recipient: The recipient's phone number, in E.123 format. + text: The text of the message to send. + block: Whether to block while sending. If you choose not to block, you won't get an exception if there + are any errors. + """ + payload = {"type": "send", "username": self.username, "recipientAddress": recipient, "messageBody": text} +# print(f"payload: {payload}") + self._send_command(payload, block) + + def send_group_message(self, recipient_group_id: str, text: str, block: bool = False) -> None: + """ + Send a group message. + + recipient_group_id: The base64 encoded group ID to send to. + text: The text of the message to send. + block: Whether to block while sending. If you choose not to block, you won't get an exception if + there are any errors. + """ + payload = { + "type": "send", + "username": self.username, + "recipientGroupId": recipient_group_id, + "messageBody": text, + } + self._send_command(payload, block) + + def chat_handler(self, regex, order=100): + """ + A decorator that registers a chat handler function with a regex. + """ + if not isinstance(regex, RE_TYPE): + regex = re.compile(regex, re.I) + + def decorator(func): + self._chat_handlers.append((order, regex, func)) + # Use only the first value to sort so that declaration order doesn't change. + self._chat_handlers.sort(key=lambda x: x[0]) + return func + + return decorator + + def run_chat(self): + """ + Start the chat event loop. + """ + for message in self.receive_messages(): + if not message.text: + continue + #print("text: " + message.text) + for _, regex, func in self._chat_handlers: + match = re.search(regex, message.text) + if not match: + continue + + try: + reply = func(message, match) + except: # noqa - We don't care why this failed. + continue + + if isinstance(reply, tuple): + stop, reply = reply + else: + stop = True + + # In case a message came from a group chat + group_id = message.group_info.get("groupId") + + if group_id: + self.send_group_message(recipient_group_id=group_id, text=reply) + else: + self.send_message(recipient=message.source, text=reply) + + if stop: + # We don't want to continue matching things. + break diff --git a/pysignald/types.py b/pysignald/types.py new file mode 100644 index 0000000..1b605b3 --- /dev/null +++ b/pysignald/types.py @@ -0,0 +1,24 @@ +import attr + + +@attr.s +class Attachment: + content_type = attr.ib(type=str) + id = attr.ib(type=str) + size = attr.ib(type=int) + stored_filename = attr.ib(type=str) + + +@attr.s +class Message: + username = attr.ib(type=str) + source = attr.ib(type=str) + text = attr.ib(type=str) + source_device = attr.ib(type=int, default=0) + timestamp = attr.ib(type=int, default=None) + timestamp_iso = attr.ib(type=str, default=None) + expiration_secs = attr.ib(type=int, default=0) + is_receipt = attr.ib(type=bool, default=False) + attachments = attr.ib(type=list, default=[]) + quote = attr.ib(type=str, default=None) + group_info = attr.ib(type=dict, default={}) -- cgit v1.2.3-54-g00ecf