aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorenz Diener <[email protected]>2018-06-05 14:10:53 +0200
committerLorenz Diener <[email protected]>2018-06-05 14:10:53 +0200
commit392dd3d61d7f8080faa56e0b7ac9bd09b21ef218 (patch)
tree0442378491710573ec21493e3d9d7c6de6317640 /mastodon
parent37cd1a489befe9e6914a77958133952066105296 (diff)
downloadmastodon.py-392dd3d61d7f8080faa56e0b7ac9bd09b21ef218.tar.gz
Add webpush support
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py170
1 files changed, 165 insertions, 5 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 17f059f..ed2a1f7 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -20,6 +20,11 @@ import threading
20import sys 20import sys
21import six 21import six
22from decorator import decorate 22from decorator import decorate
23from cryptography.hazmat.backends import default_backend
24from cryptography.hazmat.primitives.asymmetric import ec
25import http_ece
26import base64
27import json
23 28
24try: 29try:
25 from urllib.parse import urlparse 30 from urllib.parse import urlparse
@@ -126,15 +131,16 @@ class Mastodon:
126 __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS), __DICT_VERSION_HASHTAG) 131 __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS), __DICT_VERSION_HASHTAG)
127 __DICT_VERSION_ACTIVITY = "2.1.2" 132 __DICT_VERSION_ACTIVITY = "2.1.2"
128 __DICT_VERSION_REPORT = "1.1.0" 133 __DICT_VERSION_REPORT = "1.1.0"
134 __DICT_VERSION_PUSH = "2.4.0"
129 135
130 ### 136 ###
131 # Registering apps 137 # Registering apps
132 ### 138 ###
133 @staticmethod 139 @staticmethod
134 def create_app(client_name, scopes=['read', 'write', 'follow'], redirect_uris=None, website=None, to_file=None, 140 def create_app(client_name, scopes=['read', 'write', 'follow', 'push'], redirect_uris=None, website=None, to_file=None,
135 api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT): 141 api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT):
136 """ 142 """
137 Create a new app with given `client_name` and `scopes` (read, write, follow) 143 Create a new app with given `client_name` and `scopes` (read, write, follow, push)
138 144
139 Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating. 145 Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating.
140 Specify `to_file` to persist your apps info to a file so you can use them in the constructor. 146 Specify `to_file` to persist your apps info to a file so you can use them in the constructor.
@@ -303,7 +309,7 @@ class Mastodon:
303 return Mastodon.__SUPPORTED_MASTODON_VERSION 309 return Mastodon.__SUPPORTED_MASTODON_VERSION
304 310
305 def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", 311 def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob",
306 scopes=['read', 'write', 'follow']): 312 scopes=['read', 'write', 'follow', 'push']):
307 """Returns the url that a client needs to request the grant from the server. 313 """Returns the url that a client needs to request the grant from the server.
308 """ 314 """
309 if client_id is None: 315 if client_id is None:
@@ -323,7 +329,7 @@ class Mastodon:
323 329
324 def log_in(self, username=None, password=None, 330 def log_in(self, username=None, password=None,
325 code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, 331 code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None,
326 scopes=['read', 'write', 'follow'], to_file=None): 332 scopes=['read', 'write', 'follow', 'push'], to_file=None):
327 """ 333 """
328 Get the access token for a user. 334 Get the access token for a user.
329 335
@@ -950,6 +956,19 @@ class Mastodon:
950 return self.__api_request('GET', '/api/v1/custom_emojis') 956 return self.__api_request('GET', '/api/v1/custom_emojis')
951 957
952 ### 958 ###
959 # Reading data: Webpush subscriptions
960 ###
961 @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH)
962 def push_subscription(self):
963 """
964 Fetch the current push subscription the logged-in user has for this app.
965
966 Returns a `push subscription dict`_.
967
968 """
969 return self.__api_request('GET', '/api/v1/push/subscription')
970
971 ###
953 # Writing data: Statuses 972 # Writing data: Statuses
954 ### 973 ###
955 @api_version("1.0.0", "2.3.0", __DICT_VERSION_STATUS) 974 @api_version("1.0.0", "2.3.0", __DICT_VERSION_STATUS)
@@ -1455,6 +1474,8 @@ class Mastodon:
1455 """ 1474 """
1456 Update the metadata of the media file with the given `id`. `description` and 1475 Update the metadata of the media file with the given `id`. `description` and
1457 `focus` are as in `media_post()`_ . 1476 `focus` are as in `media_post()`_ .
1477
1478 Returns the updated `media dict`_.
1458 """ 1479 """
1459 id = self.__unpack_id(id) 1480 id = self.__unpack_id(id)
1460 1481
@@ -1484,6 +1505,136 @@ class Mastodon:
1484 self.__api_request('DELETE', '/api/v1/domain_blocks', params) 1505 self.__api_request('DELETE', '/api/v1/domain_blocks', params)
1485 1506
1486 ### 1507 ###
1508 # Writing data: Push subscriptions
1509 ###
1510 @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH)
1511 def push_subscription_set(self, endpoint, encrypt_params, follow_events=False,
1512 favourite_events=False, reblog_events=False,
1513 mention_events=False):
1514 """
1515 Sets up or modifies the push subscription the logged-in user has for this app.
1516
1517 `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon
1518 requires https for this URL. `encrypt_params` is a dict with key parameters that allow
1519 the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`.
1520 You can generate this as well as the corresponding private key using the
1521 `push_subscription_generate_keys()`_ .
1522
1523 The rest of the parameters controls what kind of events you wish to subscribe to.
1524
1525 Returns a `push subscription dict`_.
1526 """
1527 endpoint = Mastodon.__protocolize(endpoint)
1528
1529 push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey'])
1530 push_auth_b64 = base64.b64encode(encrypt_params['auth'])
1531
1532 params = {
1533 'subscription[endpoint]': endpoint,
1534 'subscription[keys][p256dh]': push_pubkey_b64,
1535 'subscription[keys][auth]': push_auth_b64
1536 }
1537
1538 if follow_events == True:
1539 params['data[alerts][follow]'] = True
1540
1541 if favourite_events == True:
1542 params['data[alerts][favourite]'] = True
1543
1544 if reblog_events == True:
1545 params['data[alerts][reblog]'] = True
1546
1547 if mention_events == True:
1548 params['data[alerts][mention]'] = True
1549
1550 return self.__api_request('POST', '/api/v1/push/subscription', params)
1551
1552 @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH)
1553 def push_subscription_update(self, endpoint, encrypt_params, follow_events=False,
1554 favourite_events=False, reblog_events=False,
1555 mention_events=False):
1556 """
1557 Modifies what kind of events the app wishes to subscribe to.
1558
1559 Returns the updated `push subscription dict`_.
1560 """
1561 params = {}
1562
1563 if follow_events == True:
1564 params['data[alerts][follow]'] = True
1565
1566 if favourite_events == True:
1567 params['data[alerts][favourite]'] = True
1568
1569 if reblog_events == True:
1570 params['data[alerts][reblog]'] = True
1571
1572 if mention_events == True:
1573 params['data[alerts][mention]'] = True
1574
1575 return self.__api_request('PUT', '/api/v1/push/subscription', params)
1576
1577 @api_version("2.4.0", "2.4.0", "2.4.0")
1578 def push_subscription_delete(self):
1579 """
1580 Remove the current push subscription the logged-in user has for this app.
1581 """
1582 self.__api_request('DELETE', '/api/v1/push/subscription')
1583
1584
1585 ###
1586 # Push subscription crypto utilities
1587 ###
1588 def push_subscription_generate_keys(self):
1589 """
1590 Generates a private key, public key and shared secret for use in webpush subscriptionss.
1591
1592 Returns two dicts: One with the private key and shared secret and another with the
1593 public key and shared secret.
1594 """
1595 push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend())
1596 push_key_priv = push_key_pair.private_numbers().private_value
1597 push_key_pub = push_key_pair.public_key().public_numbers().encode_point()
1598 push_shared_secret = os.urandom(16)
1599
1600 priv_dict = {
1601 'privkey': push_key_priv,
1602 'auth': push_shared_secret
1603 }
1604
1605 pub_dict = {
1606 'pubkey': push_key_pub,
1607 'auth': push_shared_secret
1608 }
1609
1610 return priv_dict, pub_dict
1611
1612 def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header):
1613 """
1614 Decrypts `data` received in a webpush request. Requires the private key dict
1615 from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the
1616 Encryption and server Crypto-Key headers from the received webpush
1617
1618 Returns the decoded webpush.
1619 """
1620 salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip())
1621 dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip())
1622 p256ecdsa = self.__decode_webpush_b64(crypto_key_header.split("p256ecdsa=")[1].strip())
1623 dec_key = ec.derive_private_key(decrypt_params['privkey'], ec.SECP256R1(), default_backend())
1624 decrypted = http_ece.decrypt(
1625 data,
1626 salt = salt,
1627 key = p256ecdsa,
1628 private_key = dec_key,
1629 dh = dhparams,
1630 auth_secret=decrypt_params['auth'],
1631 keylabel = "P-256",
1632 version = "aesgcm"
1633 )
1634
1635 return json.loads(decrypted.decode('utf-8'), object_hook = Mastodon.__json_hooks)
1636
1637 ###
1487 # Pagination 1638 # Pagination
1488 ### 1639 ###
1489 def fetch_next(self, previous_page): 1640 def fetch_next(self, previous_page):
@@ -1983,7 +2134,16 @@ class Mastodon:
1983 return id["id"] 2134 return id["id"]
1984 else: 2135 else:
1985 return id 2136 return id
1986 2137
2138 def __decode_webpush_b64(self, data):
2139 """
2140 Re-pads and decodes urlsafe base64.
2141 """
2142 missing_padding = len(data) % 4
2143 if missing_padding != 0:
2144 data += '=' * (4 - missing_padding)
2145 return base64.urlsafe_b64decode(data)
2146
1987 def __get_token_expired(self): 2147 def __get_token_expired(self):
1988 """Internal helper for oauth code""" 2148 """Internal helper for oauth code"""
1989 return self._token_expired < datetime.datetime.now() 2149 return self._token_expired < datetime.datetime.now()
Powered by cgit v1.2.3 (git 2.41.0)