Added notificactions, me and account methods

This commit is contained in:
spla 2022-08-07 16:50:15 +02:00
pare 4d19b21940
commit 1354d25b4b

158
akkoma.py
Veure arxiu

@ -77,7 +77,7 @@ class AttribAccessDict(dict):
return self[attr] return self[attr]
else: else:
raise AttributeError("Attribute not found: " + str(attr)) raise AttributeError("Attribute not found: " + str(attr))
def __setattr__(self, attr, val): def __setattr__(self, attr, val):
if attr in self: if attr in self:
raise AttributeError("Attribute-style access is read only") raise AttributeError("Attribute-style access is read only")
@ -145,7 +145,8 @@ class Akkoma:
__DICT_VERSION_POLL = "2.8.0" __DICT_VERSION_POLL = "2.8.0"
__DICT_VERSION_STATUS = bigger_version(bigger_version(bigger_version(bigger_version(bigger_version("3.1.0", __DICT_VERSION_STATUS = bigger_version(bigger_version(bigger_version(bigger_version(bigger_version("3.1.0",
__DICT_VERSION_MEDIA), __DICT_VERSION_ACCOUNT), __DICT_VERSION_APPLICATION), __DICT_VERSION_MENTION), __DICT_VERSION_POLL) __DICT_VERSION_MEDIA), __DICT_VERSION_ACCOUNT), __DICT_VERSION_APPLICATION), __DICT_VERSION_MENTION), __DICT_VERSION_POLL)
__DICT_VERSION_NOTIFICATION = bigger_version(bigger_version("1.0.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS)
@staticmethod @staticmethod
def create_app(app_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, api_base_url=__DEFAULT_BASE_URL, def create_app(app_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, api_base_url=__DEFAULT_BASE_URL,
request_timeout=__DEFAULT_TIMEOUT, session=None): request_timeout=__DEFAULT_TIMEOUT, session=None):
@ -233,13 +234,13 @@ class Akkoma:
expect to be installed on the server. The function will throw an error if an unparseable expect to be installed on the server. The function will throw an error if an unparseable
Version is specified. If no version is specified, Akkoma.py will set `akkoma_version` to the Version is specified. If no version is specified, Akkoma.py will set `akkoma_version` to the
detected version. detected version.
The version check mode can be set to "created" (the default behaviour), "changed" or "none". If set to The version check mode can be set to "created" (the default behaviour), "changed" or "none". If set to
"created", Akkoma.py will throw an error if the version of Akkoma it is connected to is too old "created", Akkoma.py will throw an error if the version of Akkoma it is connected to is too old
to have an endpoint. If it is set to "changed", it will throw an error if the endpoints behaviour has to have an endpoint. If it is set to "changed", it will throw an error if the endpoints behaviour has
changed after the version of Akkoma that is connected has been released. If it is set to "none", changed after the version of Akkoma that is connected has been released. If it is set to "none",
version checking is disabled. version checking is disabled.
`feature_set` can be used to enable behaviour specific to non-mainline Akkoma API implementations. `feature_set` can be used to enable behaviour specific to non-mainline Akkoma API implementations.
Details are documented in the functions that provide such functionality. Currently supported feature Details are documented in the functions that provide such functionality. Currently supported feature
sets are `mainline`, `fedibird` and `pleroma`. sets are `mainline`, `fedibird` and `pleroma`.
@ -247,7 +248,7 @@ class Akkoma:
self.api_base_url = None self.api_base_url = None
if not api_base_url is None: if not api_base_url is None:
self.api_base_url = Akkoma.__protocolize(api_base_url) self.api_base_url = Akkoma.__protocolize(api_base_url)
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self.access_token = access_token self.access_token = access_token
@ -255,9 +256,9 @@ class Akkoma:
self.ratelimit_method = ratelimit_method self.ratelimit_method = ratelimit_method
self._token_expired = datetime.datetime.now() self._token_expired = datetime.datetime.now()
self._refresh_token = None self._refresh_token = None
self.__logged_in_id = None self.__logged_in_id = None
self.ratelimit_limit = 300 self.ratelimit_limit = 300
self.ratelimit_reset = time.time() self.ratelimit_reset = time.time()
self.ratelimit_remaining = 300 self.ratelimit_remaining = 300
@ -274,14 +275,14 @@ class Akkoma:
self.feature_set = feature_set self.feature_set = feature_set
if not self.feature_set in ["mainline", "fedibird", "pleroma"]: if not self.feature_set in ["mainline", "fedibird", "pleroma"]:
raise AkkomaIllegalArgumentError('Requested invalid feature set') raise AkkomaIllegalArgumentError('Requested invalid feature set')
# Token loading # Token loading
if self.client_id is not None: if self.client_id is not None:
if os.path.isfile(self.client_id): if os.path.isfile(self.client_id):
with open(self.client_id, 'r') as secret_file: with open(self.client_id, 'r') as secret_file:
self.client_id = secret_file.readline().rstrip() self.client_id = secret_file.readline().rstrip()
self.client_secret = secret_file.readline().rstrip() self.client_secret = secret_file.readline().rstrip()
try_base_url = secret_file.readline().rstrip() try_base_url = secret_file.readline().rstrip()
if (not try_base_url is None) and len(try_base_url) != 0: if (not try_base_url is None) and len(try_base_url) != 0:
try_base_url = Akkoma.__protocolize(try_base_url) try_base_url = Akkoma.__protocolize(try_base_url)
@ -295,14 +296,14 @@ class Akkoma:
if self.access_token is not None and os.path.isfile(self.access_token): if self.access_token is not None and os.path.isfile(self.access_token):
with open(self.access_token, 'r') as token_file: with open(self.access_token, 'r') as token_file:
self.access_token = token_file.readline().rstrip() self.access_token = token_file.readline().rstrip()
try_base_url = token_file.readline().rstrip() try_base_url = token_file.readline().rstrip()
if (not try_base_url is None) and len(try_base_url) != 0: if (not try_base_url is None) and len(try_base_url) != 0:
try_base_url = Akkoma.__protocolize(try_base_url) try_base_url = Akkoma.__protocolize(try_base_url)
if not (self.api_base_url is None or try_base_url == self.api_base_url): if not (self.api_base_url is None or try_base_url == self.api_base_url):
raise AkkomaIllegalArgumentError('Mismatch in base URLs between files and/or specified') raise AkkomaIllegalArgumentError('Mismatch in base URLs between files and/or specified')
self.api_base_url = try_base_url self.api_base_url = try_base_url
# Versioning # Versioning
if akkoma_version == None: if akkoma_version == None:
self.retrieve_akkoma_version() self.retrieve_akkoma_version()
@ -311,11 +312,11 @@ class Akkoma:
self.akkoma_major, self.akkoma_minor, self.akkoma_patch = parse_version_string(akkoma_version) self.akkoma_major, self.akkoma_minor, self.akkoma_patch = parse_version_string(akkoma_version)
except: except:
raise AkkomaVersionError("Bad version specified") raise AkkomaVersionError("Bad version specified")
if not version_check_mode in ["created", "changed", "none"]: if not version_check_mode in ["created", "changed", "none"]:
raise AkkomaIllegalArgumentError("Invalid version check method.") raise AkkomaIllegalArgumentError("Invalid version check method.")
self.version_check_mode = version_check_mode self.version_check_mode = version_check_mode
# Ratelimiting parameter check # Ratelimiting parameter check
if ratelimit_method not in ["throw", "wait", "pace"]: if ratelimit_method not in ["throw", "wait", "pace"]:
raise AkkomaIllegalArgumentError("Invalid ratelimit method.") raise AkkomaIllegalArgumentError("Invalid ratelimit method.")
@ -335,16 +336,35 @@ class Akkoma:
self.akkoma_major, self.akkoma_minor, self.akkoma_patch = parse_version_string(version_str) self.akkoma_major, self.akkoma_minor, self.akkoma_patch = parse_version_string(version_str)
return version_str return version_str
def verify_minimum_version(self, version_str, cached=False):
"""
Update version info from server and verify that at least the specified version is present.
If you specify "cached", the version info update part is skipped.
Returns True if version requirement is satisfied, False if not.
"""
if not cached:
self.retrieve_akkoma_version()
major, minor, patch = parse_version_string(version_str)
if major > self.akkoma_major:
return False
elif major == self.akkoma_major and minor > self.akkoma_minor:
return False
elif major == self.akkoma_major and minor == self.akkoma_minor and patch > self.akkoma_patch:
return False
return True
def log_in(self, client_id=None, client_secret=None, grant_type=None, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None): def log_in(self, client_id=None, client_secret=None, grant_type=None, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None):
""" """
Get the access token for a user. Get the access token for a user.
The username is the e-mail used to log in into akkoma. The username is the e-mail used to log in into akkoma.
Can persist access token to file `to_file`, to be used in the constructor. Can persist access token to file `to_file`, to be used in the constructor.
Handles password and OAuth-based authorization. Handles password and OAuth-based authorization.
Will throw a `AkkomaIllegalArgumentError` if the OAuth or the Will throw a `AkkomaIllegalArgumentError` if the OAuth or the
username / password credentials given are incorrect, and username / password credentials given are incorrect, and
`AkkomaAPIError` if all of the requested scopes were not granted. `AkkomaAPIError` if all of the requested scopes were not granted.
@ -391,7 +411,7 @@ class Akkoma:
for scope_set in self.__SCOPE_SETS.keys(): for scope_set in self.__SCOPE_SETS.keys():
if scope_set in received_scopes: if scope_set in received_scopes:
received_scopes += self.__SCOPE_SETS[scope_set] received_scopes += self.__SCOPE_SETS[scope_set]
if not set(scopes) <= set(received_scopes): if not set(scopes) <= set(received_scopes):
raise AkkomaAPIError( raise AkkomaAPIError(
'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".')
@ -400,14 +420,80 @@ class Akkoma:
with open(to_file, 'w') as token_file: with open(to_file, 'w') as token_file:
token_file.write(response['access_token'] + "\n") token_file.write(response['access_token'] + "\n")
token_file.write(self.api_base_url + "\n") token_file.write(self.api_base_url + "\n")
self.__logged_in_id = None self.__logged_in_id = None
return response['access_token'] return response['access_token']
###
# Reading data: Notifications
###
#@api_version("1.0.0", "2.9.0", __DICT_VERSION_NOTIFICATION)
def notifications(self, id=None, account_id=None, max_id=None, min_id=None, since_id=None, limit=None, mentions_only=None):
"""
Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in
user. Pass `account_id` to get only notifications originating from the given account.
Can be passed an `id` to fetch a single notification.
Returns a list of `notification dicts`_.
"""
if max_id != None:
max_id = self.__unpack_id(max_id)
if min_id != None:
min_id = self.__unpack_id(min_id)
if since_id != None:
since_id = self.__unpack_id(since_id)
if account_id != None:
account_id = self.__unpack_id(account_id)
if id is None:
params = self.__generate_params(locals(), ['id'])
return self.__api_request('GET', '/api/v1/notifications', params)
else:
id = self.__unpack_id(id)
url = '/api/v1/notifications/{0}'.format(str(id))
return self.__api_request('GET', url)
###
# Reading data: Accounts
###
@api_version("1.0.0", "1.0.0", __DICT_VERSION_ACCOUNT)
def account(self, id):
"""
Fetch account information by user `id`.
Does not require authentication for publicly visible accounts.
Returns a `user dict`_.
"""
id = self.__unpack_id(id)
url = '/api/v1/accounts/{0}'.format(str(id))
return self.__api_request('GET', url)
@api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT)
def account_verify_credentials(self):
"""
Fetch logged-in user's account information.
Returns a `user dict`_ (Starting from 2.1.0, with an additional "source" field).
"""
return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
@api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT)
def me(self):
"""
Get this users account. Symonym for `account_verify_credentials()`, does exactly
the same thing, just exists becase `account_verify_credentials()` has a confusing
name.
"""
return self.account_verify_credentials()
### ###
# Internal helpers, dragons probably # Internal helpers, dragons probably
### ###
@staticmethod @staticmethod
def __json_allow_dict_attrs(json_object): def __json_allow_dict_attrs(json_object):
""" """
@ -435,7 +521,7 @@ class Akkoma:
except: except:
raise AkkomaAPIError('Encountered invalid date.') raise AkkomaAPIError('Encountered invalid date.')
return json_object return json_object
@staticmethod @staticmethod
def __json_truefalse_parse(json_object): def __json_truefalse_parse(json_object):
""" """
@ -887,7 +973,27 @@ class Akkoma:
params = self.__generate_params(params_initial, ['idempotency_key']) params = self.__generate_params(params_initial, ['idempotency_key'])
return self.__api_request('POST', '/api/v1/statuses', params, headers = headers, use_json = use_json) return self.__api_request('POST', '/api/v1/statuses', params, headers = headers, use_json = use_json)
###
# Writing data: Notifications
###
#@api_version("1.0.0", "1.0.0", "1.0.0")
def notifications_clear(self):
"""
Clear out a users notifications
"""
self.__api_request('POST', '/api/v1/notifications/clear')
#@api_version("1.3.0", "2.9.2", "2.9.2")
def notifications_dismiss(self, id):
"""
Deletes a single notification
"""
id = self.__unpack_id(id)
url = '/api/v1/notifications/{0}/dismiss'.format(str(id))
self.__api_request('POST', url)
### ###
# Writing data: Media # Writing data: Media
### ###
@ -924,7 +1030,7 @@ class Akkoma:
if focus != None: if focus != None:
focus = str(focus[0]) + "," + str(focus[1]) focus = str(focus[0]) + "," + str(focus[1])
media_file_description = (file_name, media_file, mime_type) media_file_description = (file_name, media_file, mime_type)
return self.__api_request('POST', '/api/v1/media', return self.__api_request('POST', '/api/v1/media',
files={'file': media_file_description}, files={'file': media_file_description},
@ -933,7 +1039,7 @@ class Akkoma:
def __unpack_id(self, id): def __unpack_id(self, id):
""" """
Internal object-to-id converter Internal object-to-id converter
Checks if id is a dict that contains id and Checks if id is a dict that contains id and
returns the id inside, otherwise just returns returns the id inside, otherwise just returns
the id straight. the id straight.
@ -952,7 +1058,7 @@ class Akkoma:
"""Internal helper for oauth code""" """Internal helper for oauth code"""
self._refresh_token = value self._refresh_token = value
return return
@staticmethod @staticmethod
def __protocolize(base_url): def __protocolize(base_url):
"""Internal add-protocol-to-url helper""" """Internal add-protocol-to-url helper"""
@ -992,6 +1098,10 @@ class AkkomaAPIError(AkkomaError):
"""Raised when the akkoma API generates a response that cannot be handled""" """Raised when the akkoma API generates a response that cannot be handled"""
pass pass
class AkkomaNotFoundError(AkkomaAPIError):
"""Raised when the akkoma API returns a 404 Not Found error"""
pass
class AkkomaMalformedEventError(AkkomaError): class AkkomaMalformedEventError(AkkomaError):
"""Raised when the server-sent event stream is malformed""" """Raised when the server-sent event stream is malformed"""
pass pass