pax_global_header00006660000000000000000000000064146075514100014515gustar00rootroot0000000000000052 comment=cc0b95c535e045661ad1c6fc399879bfb7d14cad bbernhard-pysignalclirestapi-cc0b95c/000077500000000000000000000000001460755141000200425ustar00rootroot00000000000000bbernhard-pysignalclirestapi-cc0b95c/.gitignore000066400000000000000000000000601460755141000220260ustar00rootroot00000000000000dist/ *.pyc build/ pysignalclirestapi.egg-info/ bbernhard-pysignalclirestapi-cc0b95c/LICENSE.txt000066400000000000000000000020501460755141000216620ustar00rootroot00000000000000MIT License Copyright (c) 2019 Bernhard B. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bbernhard-pysignalclirestapi-cc0b95c/README.md000066400000000000000000000001451460755141000213210ustar00rootroot00000000000000Small python library for the [Signal Cli REST API](https://github.com/bbernhard/signal-cli-rest-api) bbernhard-pysignalclirestapi-cc0b95c/pysignalclirestapi/000077500000000000000000000000001460755141000237505ustar00rootroot00000000000000bbernhard-pysignalclirestapi-cc0b95c/pysignalclirestapi/__init__.py000066400000000000000000000002001460755141000260510ustar00rootroot00000000000000from pysignalclirestapi.api import SignalCliRestApi, SignalCliRestApiError, SignalCliRestApiAuth, SignalCliRestApiHTTPBasicAuth bbernhard-pysignalclirestapi-cc0b95c/pysignalclirestapi/api.py000066400000000000000000000310521460755141000250740ustar00rootroot00000000000000"""SignalCliRestApi Python library.""" import sys import base64 import json from abc import ABC, abstractmethod from requests.models import HTTPBasicAuth from six import raise_from import requests from .helpers import bytes_to_base64 class SignalCliRestApiError(Exception): """SignalCliRestApiError base class.""" pass class SignalCliRestApiAuth(ABC): """SignalCliRestApiAuth base class.""" @abstractmethod def get_auth(): pass class SignalCliRestApiHTTPBasicAuth(SignalCliRestApiAuth): """SignalCliRestApiHTTPBasicAuth offers HTTP basic authentication.""" def __init__(self, basic_auth_user, basic_auth_pwd): self._auth = HTTPBasicAuth(basic_auth_user, basic_auth_pwd) def get_auth(self): return self._auth class SignalCliRestApi(object): """SignalCliRestApi implementation.""" def __init__(self, base_url, number, auth=None, verify_ssl=True): """Initialize the class.""" super(SignalCliRestApi, self).__init__() self._base_url = base_url self._number = number self._verify_ssl = verify_ssl if auth: assert issubclass( type(auth), SignalCliRestApiAuth), "Expecting a subclass of SignalCliRestApiAuth as auth parameter" self._auth = auth.get_auth() else: self._auth = None def about(self): resp = requests.get(self._base_url + "/v1/about", auth=self._auth, verify=self._verify_ssl) if resp.status_code == 200: return resp.json() return None def api_info(self): try: data = self.about() if data is None: return ["v1", 1] api_versions = data["versions"] build_nr = 1 try: build_nr = data["build"] except KeyError: pass return api_versions, build_nr except Exception as exc: raise_from(SignalCliRestApiError( "Couldn't determine REST API version"), exc) def has_capability(self, endpoint, capability, about=None): if about is None: about = self.about() return capability in about.get("capabilities", {}).get(endpoint, []) def mode(self): data = self.about() mode = "unknown" try: mode = data["mode"] except KeyError: pass return mode def create_group(self, name, members): try: url = self._base_url + "/v1/groups/" + self._number data = { "members": members, "name": name } resp = requests.post(url, json=data, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 201 and resp.status_code != 200: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError( "Unknown error while creating Signal Messenger group") return resp.json()["id"] except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError( "Couldn't create Signal Messenger group: "), exc) def list_groups(self): try: url = self._base_url + "/v1/groups/" + self._number resp = requests.get(url, auth=self._auth, verify=self._verify_ssl) json_resp = resp.json() if resp.status_code != 200: if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError( "Unknown error while listing Signal Messenger groups") return json_resp except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError( "Couldn't list Signal Messenger groups: "), exc) def receive(self): try: url = self._base_url + "/v1/receive/" + self._number resp = requests.get(url, auth=self._auth, verify=self._verify_ssl) json_resp = resp.json() if resp.status_code != 200: if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError( "Unknown error while receiving Signal Messenger data") return json_resp except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError( "Couldn't receive Signal Messenger data: "), exc) def update_profile(self, name, filename=None): """Update Profile. Set the name and optionally an avatar. """ try: url = self._base_url + "/v1/profiles/" + self._number data = { "name": name } if filename is not None: with open(filename, "rb") as ofile: base64_avatar = bytes_to_base64(ofile.read()) data["base64_avatar"] = base64_avatar resp = requests.put(url, json=data, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 204: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError( "Unknown error while updating profile") except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError("Couldn't update profile: "), exc) def send_message(self, message, recipients, filenames=None, attachments_as_bytes=None, mentions=None, quote_timestamp=None, quote_author=None, quote_message=None, quote_mentions=None, text_mode="normal"): """Send a message to one (or more) recipients. Additionally files can be attached. """ about = self.about() api_versions = about["versions"] endpoint = "v2/send" # fall back to old api version to stay downwards compatible. if "v2" not in api_versions: endpoint = "v1/send" if filenames is not None and len(filenames) > 1: if "v2" not in api_versions: # multiple attachments only allowed when api version >= v2 raise SignalCliRestApiError( "This signal-cli-rest-api version is not capable of sending multiple attachments. Please upgrade your signal-cli-rest-api docker container!") if mentions and not self.has_capability(endpoint, "mentions"): raise SignalCliRestApiError( "This signal-cli-rest-api version is not capable of sending mentions. Please upgrade your signal-cli-rest-api docker container!") if (quote_timestamp or quote_author or quote_message or quote_mentions) and not self.has_capability(endpoint, "quotes"): raise SignalCliRestApiError( "This signal-cli-rest-api version is not capable of sending quotes. Please upgrade your signal-cli-rest-api docker container!") url = f"{self._base_url}/{endpoint}" data = { "message": message, "number": self._number, "recipients": recipients, } if mentions: data["mentions"] = mentions if quote_timestamp: data["quote_timestamp"] = quote_timestamp if quote_author: data["quote_author"] = quote_author if quote_message: data["quote_message"] = quote_message if quote_mentions: data["quote_mentions"] = quote_mentions if "v2" in api_versions: data["text_mode"] = text_mode try: if "v2" in api_versions: if attachments_as_bytes is None: base64_attachments = [] else: base64_attachments = [ bytes_to_base64(attachment) for attachment in attachments_as_bytes ] if filenames is not None: for filename in filenames: with open(filename, "rb") as ofile: base64_attachment = bytes_to_base64(ofile.read()) base64_attachments.append(base64_attachment) data["base64_attachments"] = base64_attachments else: # fall back to api version 1 to stay downwards compatible if filenames is not None and len(filenames) == 1: with open(filenames[0], "rb") as ofile: base64_attachment = bytes_to_base64(ofile.read()) data["base64_attachment"] = base64_attachment resp = requests.post(url, json=data, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 201: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError( "Unknown error while sending signal message") except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError( "Couldn't send signal message"), exc) def list_attachments(self): """List all downloaded attachments.""" try: url = self._base_url + "/v1/attachments" resp = requests.get(url, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 200: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError("Unknown error while listing attachments") return resp.json() except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError("Couldn't list attachments: "), exc) def get_attachment(self, attachment_id): """Serve the attachment with the given id.""" try: url = self._base_url + "/v1/attachments/" + attachment_id resp = requests.get(url, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 200: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError("Unknown error while getting attachment") return resp.content except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError("Couldn't get attachment: "), exc) def delete_attachment(self, attachment_id): """Remove the attachment with the given id from filesystem.""" try: url = self._base_url + "/v1/attachments/" + attachment_id resp = requests.delete(url, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 204: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError("Unknown error while deleting attachment") except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError("Couldn't delete attachment: "), exc) def search(self, numbers): """Check if one or more phone numbers are registered with the Signal Service.""" try: url = self._base_url + "/v1/search" params = {"number": self._number, "numbers": numbers} resp = requests.get(url, params=params, auth=self._auth, verify=self._verify_ssl) if resp.status_code != 200: json_resp = resp.json() if "error" in json_resp: raise SignalCliRestApiError(json_resp["error"]) raise SignalCliRestApiError("Unknown error while searching phone numbers") return resp.json() except Exception as exc: if exc.__class__ == SignalCliRestApiError: raise exc raise_from(SignalCliRestApiError("Couldn't search for phone numbers: "), exc) bbernhard-pysignalclirestapi-cc0b95c/pysignalclirestapi/helpers.py000066400000000000000000000005641460755141000257710ustar00rootroot00000000000000"""Helper functions""" import base64 from sys import version_info def bytes_to_base64(in_bytes: bytes) -> str: """Converts bytes to base64-encoded str using the appropriate system version. """ if version_info >= (3, 0): return str(base64.b64encode(in_bytes), encoding="utf-8") else: return str(base64.b64encode(in_bytes)).encode("utf-8") bbernhard-pysignalclirestapi-cc0b95c/setup.cfg000066400000000000000000000000501460755141000216560ustar00rootroot00000000000000[metadata] description-file = README.md bbernhard-pysignalclirestapi-cc0b95c/setup.py000066400000000000000000000013571460755141000215620ustar00rootroot00000000000000import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="pysignalclirestapi", version="0.3.24", author="Bernhard B.", author_email="bernhard@liftingcoder.com", description="Small python library for the Signal Cli REST API", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/bbernhard/pysignalclirestapi", packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=2.7', install_requires=[ "requests", "six" ] )