aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNiklas Halle <niklas@niklashalle.net>2020-08-24 13:53:50 +0200
committerNiklas Halle <niklas@niklashalle.net>2020-08-24 13:53:50 +0200
commit039132d2811f111fdd6ad777f681a6221d5cc829 (patch)
tree7a47bf26b3d1f88b8e2b6200496a3dfb6e9a7174
parentc913fbd97f6d181b65bb257ae8dddca8232f9e77 (diff)
parent0ea37e19d71c6a576f7b7f207da6b0936d584d0f (diff)
downloadn_frontend_signal-039132d2811f111fdd6ad777f681a6221d5cc829.tar.gz
n_frontend_signal-039132d2811f111fdd6ad777f681a6221d5cc829.zip
Merge branch 'master' of git.niklashalle.net:n_frontend_signal into master
-rw-r--r--pysignald/__init__.py4
-rw-r--r--pysignald/__pycache__/__init__.cpython-37.pycbin0 -> 203 bytes
-rw-r--r--pysignald/__pycache__/main.cpython-37.pycbin0 -> 6450 bytes
-rw-r--r--pysignald/__pycache__/types.cpython-37.pycbin0 -> 935 bytes
-rw-r--r--pysignald/main.py212
-rw-r--r--pysignald/types.py24
6 files changed, 240 insertions, 0 deletions
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
--- /dev/null
+++ b/pysignald/__pycache__/__init__.cpython-37.pyc
Binary files differ
diff --git a/pysignald/__pycache__/main.cpython-37.pyc b/pysignald/__pycache__/main.cpython-37.pyc
new file mode 100644
index 0000000..3325cc3
--- /dev/null
+++ b/pysignald/__pycache__/main.cpython-37.pyc
Binary files differ
diff --git a/pysignald/__pycache__/types.cpython-37.pyc b/pysignald/__pycache__/types.cpython-37.pyc
new file mode 100644
index 0000000..20b838c
--- /dev/null
+++ b/pysignald/__pycache__/types.cpython-37.pyc
Binary files 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={})