From 1e9ed7db9489ddcc8238655e9235f5cca99b0adf Mon Sep 17 00:00:00 2001 From: Stavros Korokithakis Date: Thu, 6 Dec 2018 05:06:18 +0200 Subject: Initial commit --- .gitignore | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ .gitlab-ci.yml | 9 ++++ .pre-commit-config.yaml | 20 ++++++++ README.md | 35 +++++++++++++ pyproject.toml | 16 ++++++ setup.cfg | 8 +++ signald/__init__.py | 4 ++ signald/main.py | 85 ++++++++++++++++++++++++++++++++ signald/types.py | 6 +++ tests/__init__.py | 0 tests/test_signald.py | 5 ++ 11 files changed, 315 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 signald/__init__.py create mode 100644 signald/main.py create mode 100644 signald/types.py create mode 100644 tests/__init__.py create mode 100644 tests/test_signald.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb28ddb --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python Patch ### +.venv/ + +poetry.lock + +# End of https://www.gitignore.io/api/python diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..20f143e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,9 @@ +image: python:3.7 + +test: + before_script: + - pip install poetry pre-commit + - poetry install + script: + - pre-commit run -a + - poetry run pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..511cdae --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: https://github.com/ambv/black + rev: 18.9b0 + hooks: + - id: black + args: [--line-length=120] +- repo: git://github.com/doublify/pre-commit-isort + rev: v4.3.0 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8~=3.6.0] +- repo: git://github.com/skorokithakis/pre-commit-mypy + rev: v0.610 + hooks: + - id: mypy + args: [-s] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d6755a --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +pysignald +======= + +[![pipeline status](https://gitlab.com/stavros/pysignald/badges/master/pipeline.svg)](https://gitlab.com/stavros/pysignald/commits/master) + +pysignald is a Python client for the excellent [signald](https://git.callpipe.com/finn/signald) project, which in turn +is a command-line client for the Signal messaging service. + +pysignald allows you to programmatically send and receive messages to Signal. + +Installation +------------ + +You can install pysignald with pip: + +``` +$ pip install signald +``` + + +Running +------- + +Just make sure you have signald installed. Here's an example of how to use pysignald: + + +```python +from signald import Signal + +s = Signal("+1234567890") +s.send_message("+1098765432", "Hello there!") + +for message in s.receive_messages(): + print(message) +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..25bbeef --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "signald" +version = "0.0.1" +description = "" +authors = ["Stavros Korokithakis "] + +[tool.poetry.dependencies] +python = "^3.4" +attrs = "^18.2" + +[tool.poetry.dev-dependencies] +pytest = "^3.5" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..135eb6d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +ignore=F403,E128,E126,E123,E121,E265,E501,N802,N803,N806,C901 + +[isort] +include_trailing_comma = true +line_length = 120 +force_grid_wrap = 0 +multi_line_output = 3 diff --git a/signald/__init__.py b/signald/__init__.py new file mode 100644 index 0000000..2066083 --- /dev/null +++ b/signald/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +__version__ = "0.0.1" + +from .main import Signal diff --git a/signald/main.py b/signald/main.py new file mode 100644 index 0000000..61ae1a5 --- /dev/null +++ b/signald/main.py @@ -0,0 +1,85 @@ +import json +import random +import socket +from typing import Iterator, List # noqa + +from .types import Message + + +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 + + 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." + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(self.socket_path) + return s + + def _send_command(self, payload: dict): + 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") + + 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): + payload = {"type": "register", "username": self.username, "voice": voice} + self._send_command(payload) + + def verify(self, code: str): + 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": + continue + + yield message + + def send_message(self, recipient: str, message: str) -> None: + payload = {"type": "send", "username": self.username, "recipientNumber": recipient, "messageBody": message} + self._send_command(payload) diff --git a/signald/types.py b/signald/types.py new file mode 100644 index 0000000..814b967 --- /dev/null +++ b/signald/types.py @@ -0,0 +1,6 @@ +import attr + + +@attr.s +class Message: + pass diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_signald.py b/tests/test_signald.py new file mode 100644 index 0000000..8824272 --- /dev/null +++ b/tests/test_signald.py @@ -0,0 +1,5 @@ +from signald import Signal + + +def test_signal(): + Signal("+1234567890") -- cgit v1.2.3-54-g00ecf