Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • briar/python-briar-wrapper
1 result
Show changes
Commits on Source (2)
Showing
with 247 additions and 33 deletions
.coverage
__pycache__
dist
docs/briar_wrapper
......@@ -59,6 +59,11 @@ And install the .deb like this:
sudo dpkg -i ../python3-briar-wrapper_0.0.3-1_all.deb
```
## Documentation
The documentation of the project's code can be found in the _docs_ directory.
To generate it, call `tools/generate-docs.sh`.
## Design Goals
* Main platform is GNU/Linux, but should also support (at least) Windows and macOS
......
......@@ -2,6 +2,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""Wrapper for the Briar Headless REST API"""
"""
Wrapper for the Briar Headless REST API
Before using `briar_wrapper` you need to initialize an instance of
`briar_wrapper.api.Api`. You can check with `briar_wrapper.api.Api.has_account`
whether you want want to `briar_wrapper.api.Api.login` or
`briar_wrapper.api.Api.register`. Now you can start to use the wrappers in
`briar_wrapper.models`.
"""
__version__ = "0.0.3"
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Central API wrapper handling login and registration
"""
import os
from threading import Thread
......@@ -12,28 +15,66 @@ from briar_wrapper.models.socket_listener import SocketListener
class Api:
# pylint: disable=line-too-long
auth_token = None
"""
Briar's authentication token
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#how-to-use)
"""
socket_listener = None
"""
`briar_wrapper.api.Api`'s instance of
`briar_wrapper.models.socket_listener.SocketListener`
"""
_api_thread = None
def __init__(self, headless_jar):
"""
Initialize with path to Briar Headless JAR `headless_jar`
"""
self._api_thread = ApiThread(self, headless_jar)
@staticmethod
def has_account():
"""
Checks if `briar_wrapper.constants.BRIAR_DB` exists
.. versionadded:: 0.0.3
"""
return os.path.isfile(BRIAR_DB)
def is_running(self):
"""
Returns `True` if `briar_wrapper.api_thread.ApiThread` is running
.. versionadded:: 0.0.3
"""
return self._api_thread.is_running()
def login(self, password, callback):
"""
Login to Briar API with `password`.
Calls `callback` once login process finished.
.. versionadded:: 0.0.3
"""
self._start_and_watch(callback)
startup_thread = Thread(target=self._api_thread.login,
args=(password,), daemon=True)
startup_thread.start()
def register(self, credentials, callback):
"""
Register at Briar API with 2-tuple `credentials`.
Calls `callback` once registration process finished.
.. versionadded:: 0.0.3
"""
if len(credentials) != 2:
raise Exception("Can't process credentials")
self._start_and_watch(callback)
......@@ -42,6 +83,11 @@ class Api:
startup_thread.start()
def stop(self):
"""
Stops API wrapper
.. versionadded:: 0.0.3
"""
self._api_thread.stop()
def _start_and_watch(self, callback):
......@@ -49,6 +95,12 @@ class Api:
self._api_thread.watch(callback)
def on_successful_startup(self, callback):
"""
Called by `briar_wrapper.api_thread.ApiThread` if startup finished
successfully.
Should not be called from outside `briar_wrapper`.
"""
self._load_auth_token()
self.socket_listener = SocketListener(self)
callback(True)
......
# Copyright (c) 2020 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Handles background thread for `briar_wrapper.api.Api`
**Should not be called from outside `briar_wrapper`.**
"""
from subprocess import Popen, PIPE, STDOUT
from threading import Thread
......@@ -17,29 +22,48 @@ class ApiThread:
_process = None
def __init__(self, api, headless_jar):
"""
Initialize with `briar_wrapper.api.Api` instance `api` and
path to Briar Headless JAR `headless_jar`
"""
self._api = api
self._command = ["java", "-jar", headless_jar]
def is_running(self):
"""
Returns `True` if background thread is runnning
"""
return (self._process is not None) and (self._process.poll() is None)
def start(self):
"""
Starts background thread
"""
if self.is_running():
raise Exception("API already running")
self._process = Popen(self._command, stdin=PIPE,
stdout=PIPE, stderr=STDOUT)
def watch(self, callback):
"""
Watches startup of background thread and calls `callback` once finished
"""
watch_thread = Thread(target=self._watch_thread, args=(callback,),
daemon=True)
watch_thread.start()
def login(self, password):
"""
Actually logins to Briar API with `password`
"""
if not self.is_running():
raise Exception("Can't login; API not running")
self._process.communicate((f"{password}\n").encode("utf-8"))
def register(self, credentials):
"""
Actually registers new account at Briar API with 2-tuple `credentials`
"""
if not self.is_running():
raise Exception("Can't register; API not running")
self._process.communicate((credentials[0] + '\n' +
......@@ -47,6 +71,9 @@ class ApiThread:
credentials[1] + '\n').encode("utf-8"))
def stop(self):
"""
Stops background thread
"""
if not self.is_running():
raise Exception("Nothing to stop")
self._process.terminate()
......
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Constants used in `briar_wrapper`
"""
from os.path import join
from pathlib import Path
......@@ -14,7 +17,21 @@ _HOST = "%s://localhost:7000"
_VERSION_SUFFIX = "v1/"
BRIAR_AUTH_TOKEN = join(_BRIAR_DIR, "auth_token")
"""
Path to Briar's authentication token
"""
BRIAR_DB = join(_BRIAR_DIR, "db", "db.mv.db")
"""
Path to Briar's database
"""
BASE_HTTP_URL = urljoin(_HOST % "http", _VERSION_SUFFIX)
"""
Base URL to construct resource's URLs
"""
WEBSOCKET_URL = urljoin(_HOST % "ws", "%s/ws" % _VERSION_SUFFIX)
"""
Websocket URL used in `briar_wrapper.models.socket_listener.SocketListener`
"""
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Abstract base class for all `briar_wrapper.models`
"""
class Model: # pylint: disable=too-few-public-methods
......@@ -8,6 +11,9 @@ class Model: # pylint: disable=too-few-public-methods
_headers = {}
def __init__(self, api):
"""
Initialize with `briar_wrapper.api.Api` instance `api`
"""
self._api = api
self._initialize_headers()
......
# Copyright (c) 2020 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Contains wrappers around different resources
"""
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Wrapper around Briar API's _/contacts/_ resource
"""
from operator import itemgetter
from urllib.parse import urljoin
......@@ -14,32 +17,66 @@ from briar_wrapper.model import Model
class Contacts(Model):
API_ENDPOINT = "contacts/"
CONNECTION_EVENTS = ("ContactConnectedEvent", "ContactDisconnectedEvent")
_API_ENDPOINT = "contacts/"
_CONNECTION_EVENTS = ("ContactConnectedEvent", "ContactDisconnectedEvent")
_connections_callback = None
def add_pending(self, link, alias):
url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT + "add/pending/")
# pylint: disable=line-too-long
"""
Adds pending contact to Briar with `link` URL and `alias`
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#adding-a-contact)
.. versionadded:: 0.0.3
"""
url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT + "add/pending/")
_post(url, headers=self._headers, json={"link": link, "alias": alias})
def get(self):
url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT)
# pylint: disable=line-too-long
"""
Returns sorted list containing all contacts
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#listing-all-contacts)
.. versionadded:: 0.0.3
.. versionchanged:: 0.0.4
"""
url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT)
request = _get(url, headers=self._headers)
contacts = request.json()
contacts = Contacts._sort_contact_list(contacts)
return contacts
def get_link(self):
url = urljoin(BASE_HTTP_URL, self.API_ENDPOINT + "add/link/")
# pylint: disable=line-too-long
"""
Returns _briar://_ link
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#adding-a-contact)
.. versionadded:: 0.0.3
"""
url = urljoin(BASE_HTTP_URL, self._API_ENDPOINT + "add/link/")
request = _get(url, headers=self._headers).json()
return request['link']
def watch_connections(self, callback):
# pylint: disable=line-too-long
"""
Calls `callback` whenever a contact's connection status changes
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#a-contact-connected-or-disconnected)
.. versionadded:: 0.0.4
"""
self._connections_callback = callback
signal_ids = list()
event_callback = self._handle_connections_callback
for event in self.CONNECTION_EVENTS:
for event in self._CONNECTION_EVENTS:
signal_id = self._api.socket_listener.connect(event,
event_callback)
signal_ids.append(signal_id)
......@@ -54,6 +91,7 @@ class Contacts(Model):
else:
raise Exception(f"Wrong event in callback: {message['name']}")
@staticmethod
def _sort_contact_list(contacts):
contacts.sort(key=itemgetter("lastChatActivity"),
reverse=True)
......
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Wrapper around Briar API's _/messages/_ resource
"""
from urllib.parse import urljoin
......@@ -12,19 +15,38 @@ from briar_wrapper.model import Model
class PrivateChat(Model):
API_ENDPOINT = "messages/"
_API_ENDPOINT = "messages/"
def __init__(self, api, contact_id):
"""
Initialize with `briar_wrapper.api.Api` instance `api` and `contact_id`
"""
super().__init__(api)
self._contact_id = contact_id
def get(self):
# pylint: disable=line-too-long
"""
Returns list containing all messages from contact
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#listing-all-private-messages)
.. versionadded:: 0.0.3
"""
url = urljoin(BASE_HTTP_URL,
self.API_ENDPOINT + str(self._contact_id))
self._API_ENDPOINT + str(self._contact_id))
request = requests.get(url, headers=self._headers)
return request.json()
def send(self, message):
# pylint: disable=line-too-long
"""
Sends `message` to contact
[Upstream documentation](https://code.briarproject.org/briar/briar/blob/master/briar-headless/README.md#writing-a-private-message)
.. versionadded:: 0.0.3
"""
url = urljoin(BASE_HTTP_URL,
self.API_ENDPOINT + str(self._contact_id))
self._API_ENDPOINT + str(self._contact_id))
requests.post(url, headers=self._headers, json={"text": message})
# Copyright (c) 2019 Nico Alt
# SPDX-License-Identifier: AGPL-3.0-only
# License-Filename: LICENSE.md
"""
Wrapper around Briar API's websocket stream
"""
import asyncio
import json
......@@ -12,22 +15,44 @@ from briar_wrapper.constants import WEBSOCKET_URL
from briar_wrapper.model import Model
class SocketListener(): # pylint: disable=too-few-public-methods
class SocketListener(Model): # pylint: disable=too-few-public-methods
def __init__(self, api):
self._api = api
super().__init__(api)
self._signals = dict()
self._signals_lock = Lock()
self._highest_signal_id = -1
self._start_websocket_thread()
def connect(self, event, callback):
"""
Connects to one of websocket API's `event`s. If the websocket API sends
out a message with given `event`, `callback` will be called.
Returns
-------
int
Signal ID used for
`briar_wrapper.models.socket_listener.SocketListener.disconnect`
later on
.. versionadded:: 0.0.3
"""
self._signals_lock.acquire()
signal_id = self._add_signal(event, callback)
self._signals_lock.release()
return signal_id
def disconnect(self, signal_id):
"""
Disconnect signal with `signal_id` from
`briar_wrapper.models.socket_listener.SocketListener`. The `callback`
given at
`briar_wrapper.models.socket_listener.SocketListener.connect` will not
be called anymore.
.. versionadded:: 0.0.3
"""
self._signals_lock.acquire()
self._remove_signal(signal_id)
self._signals_lock.release()
......@@ -38,7 +63,7 @@ class SocketListener(): # pylint: disable=too-few-public-methods
self._signals[signal_id] = {
"event": event,
"callback": callback
}
}
return signal_id
def _remove_signal(self, signal_id):
......
......@@ -9,3 +9,5 @@ pytest>=5.2.1
pytest-cov>=2.8.1
pytest-mock>=1.11.1
requests_mock>=1.7.0
pdoc3
[pycodestyle]
ignore = E501, W504
......@@ -101,7 +101,7 @@ def test_get_link(api, request_headers, requests_mock):
def test_watch_signal_added(api, mocker):
contacts = Contacts(api)
contacts._api.socket_listener = SocketListener(None)
contacts._api.socket_listener = SocketListener(api)
contacts._api.socket_listener._highest_signal_id = 136
assert contacts._api.socket_listener._signals == dict()
......
......@@ -9,30 +9,30 @@ from briar_wrapper.models.socket_listener import SocketListener
MODULE = "briar_wrapper.models.socket_listener.%s"
def test_init_websocket_thread(mocker):
def test_init_websocket_thread(api, mocker):
thread_mock = mocker.patch(MODULE % "Thread")
SocketListener(None)
SocketListener(api)
thread_mock.assert_called_once()
def test_init_watch_loop(mocker):
def test_init_watch_loop(api, mocker):
watch_loop_mock = mocker.patch(MODULE % "SocketListener._start_watch_loop")
SocketListener(None)
SocketListener(api)
watch_loop_mock.assert_called_once()
def test_connect_lock(mocker):
def test_connect_lock(api, mocker):
lock_mock = mocker.Mock()
manager = mocker.Mock()
manager.attach_mock(lock_mock.acquire, 'acquire_mock')
manager.attach_mock(lock_mock.release, 'release_mock')
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._signals_lock = lock_mock
socket_listener.connect(None, None)
......@@ -45,11 +45,11 @@ def test_connect_lock(mocker):
assert manager.mock_calls == expected_calls
def test_connect_signal_added(mocker):
def test_connect_signal_added(api, mocker):
event_mock = mocker.MagicMock()
callback_mock = mocker.MagicMock()
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._highest_signal_id = 136
assert socket_listener._signals == dict()
......@@ -66,11 +66,11 @@ def test_connect_signal_added(mocker):
assert socket_listener._signals == expected_signals
def test_connect_signal_id(mocker):
def test_connect_signal_id(api, mocker):
event_mock = mocker.MagicMock()
callback_mock = mocker.MagicMock()
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._highest_signal_id = 137
signal_id = socket_listener.connect(event_mock, callback_mock)
......@@ -78,14 +78,14 @@ def test_connect_signal_id(mocker):
assert signal_id == 137 + 1
def test_disconnect_lock(mocker):
def test_disconnect_lock(api, mocker):
lock_mock = mocker.Mock()
manager = mocker.Mock()
manager.attach_mock(lock_mock.acquire, 'acquire_mock')
manager.attach_mock(lock_mock.release, 'release_mock')
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._signals_lock = lock_mock
socket_listener._signals = mocker.MagicMock()
......@@ -99,8 +99,8 @@ def test_disconnect_lock(mocker):
assert manager.mock_calls == expected_calls
def test_disconnect_signal_removed(mocker):
socket_listener = SocketListener(None)
def test_disconnect_signal_removed(api, mocker):
socket_listener = SocketListener(api)
socket_listener._signals = {
137: {
"event": None,
......@@ -123,13 +123,13 @@ def test_watch_messages():
pass
def test_call_signal_callbacks(mocker):
def test_call_signal_callbacks(api, mocker):
message = {
'name': 'VeryImportantEvent'
}
callback_mock = mocker.MagicMock()
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._signals = {
137: {
"event": 'VeryImportantEvent',
......@@ -142,14 +142,14 @@ def test_call_signal_callbacks(mocker):
callback_mock.assert_called_once_with(message)
def test_call_signal_callbacks_lock(mocker):
def test_call_signal_callbacks_lock(api, mocker):
lock_mock = mocker.Mock()
manager = mocker.Mock()
manager.attach_mock(lock_mock.acquire, 'acquire_mock')
manager.attach_mock(lock_mock.release, 'release_mock')
socket_listener = SocketListener(None)
socket_listener = SocketListener(api)
socket_listener._signals_lock = lock_mock
socket_listener._signals = mocker.MagicMock()
......
......@@ -23,5 +23,5 @@ def auth_token():
def request_headers(auth_token):
request_headers = {
"Authorization": 'Bearer %s' % auth_token
}
}
return request_headers
#!/bin/bash
rm -fr docs/briar_wrapper
pdoc3 --html --force --output-dir docs briar_wrapper