PK!5-??signald/__init__.py# flake8: noqa __version__ = "0.0.5" from .main import Signal PK!#5signald/main.pyimport 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") 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") if message.get("type") != "message" or ( not message["data"]["isReceipt"] and message["data"]["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["dataMessage"] if message["dataMessage"] else {} yield Message( username=message["username"], source=message["source"], text=data_message.get("message"), source_device=message["sourceDevice"], timestamp=data_message.get("timestamp"), timestamp_iso=message["timestampISO"], expiration_secs=data_message.get("expiresInSeconds"), is_receipt=message["isReceipt"], 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, "recipientNumber": recipient, "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 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 self.send_message(recipient=message.source, text=reply) if stop: # We don't want to continue matching things. break PK!4 {{signald/types.pyimport 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) PK!HnHTUpysignald-0.0.5.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H+1̘ "pysignald-0.0.5.dist-info/METADATAVmo6_qUWdK_h$-P 1#6gHʮ(ʎ5qsw=w3E.})Gp\rƩEζ>.Ka7#x BxEa2SVxVJ੄ˀ3pҮT&vJYќ5UbA,ܨߟ+_}gM/eck #\*{@t"qe؅VV aLJ(}vΩh.'𲪬YɜGޕ̭(KTyɑ}㉾՞o<;_)G :' }6|]H>\BV)o&z."NRզBxQܬ5c; c}T% Qx^!7"KKo1[ͻ?@| ѽ(1+&3cCwϙ, z)ZEΔhօ9Be P|m5 (^ YK霘Sc!vߘ!WAh_+3V2IG֍h(¬`[DLh:S16NOkpE5C)XBvS(ɜ[iC$KcDY fM)80rW51cN'O >LfpC)EԄy Bn;Sی9ZwVK5~# '<\tWl%rsKB >y I!y#xt}IіDDBM[৲JN|m*iX#ez.3c6>b3 z+dK36v<t2d+[y̌\bu&t|E-E=2 OAa ;KjBmfa,J21=Z;7f܄VVh/(p%m u @t8Os8DF J .d4iAyup0ajQ;KBn6M5P KCa N-oWqW(8Dڄ$A[p#a4dB%-:ӈ*qc_ %: Ao)k0+U`GF4vݤ\+=4|nM]uoD$ ^Do~F=ƤAaHMK4}amGhiKvzk DKzMf4LP9Oha']S8> LvnYF\f(j-Q 77x嚮Jؾ۾!CN_ik80r?o8SNi؆vv p>jtأP&MfGѐC`z'_\BRR i5IdE0J^',Wh\UxBFPK!HMŸ$ pysignald-0.0.5.dist-info/RECORD}Kr0нg$`.HM7 Ш|ӷѩ+Y^ m@&}T^> pTStwWY)ܐSuH8E۔xXXWgQnz~%8bK%59d L>^X7m=DchGaJ<5 ?=-QEH7jM(<-r=:⭖gK!SӔ\MF)gʳs/VKw\tci|xv>9HH 3ɒ>}e;~PK!5-??signald/__init__.pyPK!#5psignald/main.pyPK!4 {{Fsignald/types.pyPK!HnHTUpysignald-0.0.5.dist-info/WHEELPK!H+1̘ "pysignald-0.0.5.dist-info/METADATAPK!HMŸ$ X"pysignald-0.0.5.dist-info/RECORDPK#