pax_global_header 0000666 0000000 0000000 00000000064 13371050132 0014505 g ustar 00root root 0000000 0000000 52 comment=c0c23006dc746a19d25c9427bc614a8add03d06c
diaspy-0.6.0/ 0000775 0000000 0000000 00000000000 13371050132 0013001 5 ustar 00root root 0000000 0000000 diaspy-0.6.0/.gitignore 0000664 0000000 0000000 00000000674 13371050132 0015000 0 ustar 00root root 0000000 0000000 *~
*.swp
docs/build/*
*/__pycache__/*
__pycache__/*
.env
check-*.py
# Testing
testconf.py
TEST_COUNT
sketch.py
# Private stuff
private.*
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
diaspy-0.6.0/Changelog.markdown 0000664 0000000 0000000 00000033211 13371050132 0016434 0 ustar 00root root 0000000 0000000 ## Changelog for `diaspy`, unofficial DIASPORA\* interface for Python
This changelog file follows few rules:
* __rem__: indicates removed features,
* __new__: indicates new features,
* __upd__: indicates updated features,
* __dep__: indicates deprecated features,
Deprecation means that in the next version feature will be removed.
Also, after every version there should be a brief note describing possible
problems with migrating to it from older versions and usage of new features.
Users can always read the manual and dcumentation to make themselves more knowledgeable and
are encouraged to do so. They only need to remember that documentation is usually more
up-to-date than manual and if conflicts appear they should follow the order:
*docstrings* -> *docs/* -> *manual/*
----
#### Known issues
* __bug__: `diaspy` has problems/can't connect to pods using SNI (this is an issue with requests/urllib3/python),
----
#### Version `0.6.0`
In this release some bugs due to Diaspora changes where adressed, it also
contains some new functionality. Also if `BeautifulSoup4` is installed it
will use it instead of the regex solution where possible. Also some manual
adjustments.
IMPORTANT: `python-dateutil` is a requirement now.
Note: In this version indentation changed from spaces to tabs.
* __upd__: `diaspy.people.User()`'s `fetchguid()` method can now be called with a parameter (`bool`), if set `False` it won't fetch the stream but only userdata, default it still does.,
* __upd__: `diaspy.people.User()` has new methods `getPhotos()` and `deletePhoto()`,
* __upd__: Aspect `id` is now removed from `diaspy.people.User()` object when removed,
* __upd__: `diaspy.people.Contacts()` it's `get()` method has the `page` parameter now,
* __upd__: It is now optional to automatic fetch contacts for `diaspy.people.Contacts()`, default it won't,
* __upd__: `diaspy.models.Notification()`'s `who()` method now return whole `guid`s instead of partial `guid`s,
* __upd__: Update `diaspy.models.Post()` it's interaction data after liked,
* __upd__: `diaspy.connection.Connection()`'s `getUserData()` method will now set the `Connection()` object it's `self._userdata`,
* __upd__: Posts obtained by `diaspy.streams.Generic()` are now fetched once instead of twice,
* __upd__: `tests.py`,
* __fix__: Streams seemed to miss posts on `more()` method, should be fixed now. Also a new dependency: `dateutil`,
* __fix__: Fixes `diaspy.streams.Generic()`'s `more()` and `update()` methods and adds `id` to posts,
* __fix__: `diaspy.streams.Aspect()` its `filter()` method,
* __fix__: `diaspy.models.Notification()`'s `who()` method it's regex pattern didn't always match, now it should,
* __fix__: `diaspy.models.Aspect()` its `addUser()` method did cause CSRF errors,
* __fix__: `diaspy.people.User()` its `getHCard()`,
* __new__: `diaspy.errors.SearchError()` and `diaspy.errors.TagError()`,
* __new__: `update()` and `more()` methods for `diaspy.notifications.Notifications`,
* __new__: `removeAspect()` method in `diaspy.models.Aspect()`,
* __new__: `diaspy.models.Comments()` class,
* __new__: `diaspy.models.Conversation()` has new methods `messages()` and `update_messages()`, it is also posible to call `len()` and iterate over the object,
* __new__: `diaspy.models.Post()`'s `comments` object is now a `Comments()` object instead of a `dict` (parsed json),
* __new__: `diaspy.models.Post()` has some new methods: `vote_poll()`, `hide()`, `mute()`, `subscribe()` and `unsubscribe()`,
* __new__: It is now possible to set `diaspy.people.User()` it's data manual by the `data` parameter,
* __new__: `diaspy.people.Contacts()` has new methods `add()` and `remove()` wich can add/remove a user to/from an aspect,
* __new__: Added BeautifulSoup4 (optional) support where possible instead of regex, kept regex as fallback,
* __new__: `diaspy.connection.Connection().podswitch()` has now a optional param `login` with as default set to `True`, if `False` it will only set the data and does not call `login()`,
* __rem__: `_obtain()` from `diaspy.streams.Outer()`, it was the same as `_obtain()` in `diaspy.streams.Generic()`,
* __rem__: `diaspy.models.Post()` its `update()` method since it is deprecated for a while now,
* __rem__: `backtime` parameter removed from `diaspy.streams.Generic.more()`,
* __rem__: `protocol` parameter removed from `diaspy.people.User().fetchhandle()`.
----
#### Version `0.5.0.1`
This is a hotfix release.
Plain 0.5.0 lost compatibility with older versions of Diaspora* due to a trivial assignment-related bug.
----
#### Version `0.5.0`
Release 0.5.0
This release fixes a bug that arose with Diaspora* 0.5.0 update which
changed the way how the CSRF tokens have been embedded in HTML code.
This required minor fix to the CSRF-extracting regex.
Not much besides. Fixed a typo or two.
----
#### Version `0.4.3`:
* __new__: `people.User().fetchprofile()` will issue a warning when user cannot be found on current pod,
* __new__: `settings.Profile` is now loaded during initialization (can be switched off),
* __fix__: fixed a bug in `__repr__()` method in `people.User()` object,
----
#### Version `0.4.2` (2013-12-19):
This version has some small incompatibilities with `0.4.1` so read Changelog carefully.
* __new__: `diaspy.people.User._fetchstream()` method,
* __new__: `diaspy.people.Me()` object representing current user,
* __new__: `**kwargs` added to `diaspy.streams.Generic.json()` methdo to give developers control over the creation of JSON,
* __new__: `.getHCard()` method added to `diaspy.people.User()`,
* __upd__: `diaspy.connection.Connection.login()` modifies connection object in-place **and** returns it (this allows more fluent API),
* __upd__: `diaspy.connection.Connection.login()` no longer returns status code (if login was unsuccessful it'll raise an exception),
* __upd__: `diaspy.connection.Connection._login()` no longer returns status code (if login was unsuccessful it'll raise an exception),
* __upd__: better error message in `diaspy.models.Post().__init__()`,
* __upd__: `data` variable in `diaspy.models.Post()` renamed to `_data` to indicate that it's considered private,
* __upd__: after deleting a post `Activity` stream is purged instead of being refilled (this preserves state of stream which is not reset to last 15 posts),
* __upd__: `filterByIDs()` method in `Aspects` stream renamed to `filter()`,
* __rem__: `diaspy.connection.Connection.getUserInfo()` moved to `diaspy.connection.Connection.getUserData()`,
* __rem__: `fetch` parameter removed from `diaspy.connection.Connection.getUserData()`,
* __dep__: `max_time` parameter in `diaspy.streams.*.more()` method is deprecated,
* __fix__: this release should fix the bug which prevented diaspy from working with some pods (e.g. diasp.eu and joindiaspora.com),
----
#### Version `0.4.1` (2013-09-12):
Login and authentication procedure backend received major changes in this version.
There are no longer `username` and `password` variables in `Connection` object.
Instead, credentials are stored (together with the token) in single variable `_login_data`.
This is preserved until you call `login()` at which point credentials are erased and
only token is left -- it can be obtained by calling `repr(Connection)`.
Also, this release is compatible with DIASPORA\* 0.2.0.0 but should still support
pods running on older versions.
And the test suite was updated. Yay!
* __new__: `diaspy.errors.SettingsError`.
* __upd__: `diaspy.settings.Account.setEmail()` can now raise `SettingsError` when request fails,
* __upd__: `diaspy.settings.Account.getEmail()` will now return empty string instead of raising an exception if cannot fetch mail,
* __upd__: improved language fetching in `diaspy.settings.Account.getLanguages()`.
* __rem__: `diaspy/client.py` is removed,
**`0.4.1-rc.3` (2013-09-08):**
* __new__: `diaspy.settings.Profile.load()` method for loading profile information,
* __new__: `diaspy.settings.Profile.update()` method for updating profile information,
* __new__: `diaspy.settings.Profile.setName()` method,
* __new__: `diaspy.settings.Profile.setBio()` method,
* __new__: `diaspy.settings.Profile.setLocation()` method,
* __new__: `diaspy.settings.Profile.setTags()` method,
* __new__: `diaspy.settings.Profile.setGender()` method,
* __new__: `diaspy.settings.Profile.setBirthDate()` method,
* __new__: `diaspy.settings.Profile.setSearchable()` method,
* __new__: `diaspy.settings.Profile.setNSFW()` method,
**`0.4.1-rc.2` (2013-09-06):**
* __new__: `diaspy.search.Search.tags()` method for getting tag suggestions,
* __new__: `diaspy.settings.Profile.getName()` method,
* __new__: `diaspy.settings.Profile.getBio()` method,
* __new__: `diaspy.settings.Profile.getLocation()` method,
* __new__: `diaspy.settings.Profile.getTags()` method,
* __new__: `diaspy.settings.Profile.getGender()` method,
* __new__: `diaspy.settings.Profile.getBirthDate()` method,
* __new__: `diaspy.settings.Profile.isSearchable()` method,
* __new__: `diaspy.settings.Profile.isNSFW()` method,
* __new__: `provider_display_name` parameter in `diaspy.streams.Stream.post()` (thanks @svbergerem),
* __upd__: `remeber_me` parameter in `diaspy.connection.Connection.login()`,
* __upd__: you must supply `username` and `password` parameters on init of `diaspy.connection.Connection`,
* __upd__: you must update your testconf.py (new fields are required for settings tests),
* __upd__: `diaspy.settings.Settings` renamed to `diaspy.settings.Account`,
* __rem__: `username` and `password` parameters removed from `diaspy.connection.Connection.login()`
must be supplied on init,
**`0.4.1-rc.1` (2013-09-02):**
* __new__: `__getitem__()` in `diaspy.models.Post`,
* __new__: `__dict__()` in `diaspy.models.Post`,
* __new__: `guid` argument in `diaspy.models.Post.__init__()`,
* __new__: `json()` method in `diaspy.streams.Generic` adds the possibility to export streams to JSON,
* __new__: `full()` method in `diaspy.streams.Generic` will try to fetch full stream (containing all posts),
* __new__: `setEmail()` method in `diaspy.settings.Settings`,
* __new__: `setLanguage()` method in `diaspy.settings.Settings`,
* __new__: `downloadPhotos()` method in `diaspy.settings.Settings`,
* __new__: `backtime` argument in `more()` method in `diaspy.streams.Generic`,
* __new__: `DiaspyError` will be raised when connection is created with empty password and/or username,
* __new__: `getSessionToken()` method in `diaspy.connection.Connection` returns string from `_diaspora_session` cookie,
* __new__: `direct` parameter in `diaspy.connection.Connection().get()` allowing to disable pod expansion,
* __upd__: if `Post()` is created with fetched comments, data will also be fetched as a dependency,
* __upd__: `id` argument type is now `int` (`diaspy.models.Post.__init__()`),
* __upd__: `Search().lookup_user()` renamed to `Search().lookupUser()`,
* __upd__: `diaspy.messages` renamed to `diaspy.conversations` (but will be accessible under both names for this and next release),
* __upd__: `LoginError` moved to `diaspy.errors`,
* __upd__: `TokenError` moved to `diaspy.errors`,
* __upd__: `diaspy.connection.Connection.podswitch()` gained two new positional arguments: `username` and `password`,
* __upd__: `aspect_id` renamed to `id` in `diaspy.streams.Aspects().remove()`,
* __fix__: fixed some bugs in regular expressions used by `diaspy` internals (html tag removal, so you get nicer notifications),
* __fix__: fixed authentication issues,
----
#### Version `0.4.0` (2013-08-20):
This release is **not backwards compatible with `0.3.x` line**! You'll have to check your code for corrections.
Also, this release if first to officially released fork version.
* __dep__: `diaspy.client` is officially deprecated (will be removed in `0.4.1`),
* __upd__: `diaspy.conversations` renamed to `diaspy.messages`,
* __udp__: `diaspy.conversations.Conversation` moved to `diaspy.models`,
* __new__: `diaspy.messages.Mailbox()` object representing diaspora\* mailbox,
----
#### Version `0.3.2` (2013-08-20):
* __upd__: `diaspy.connection.getUserData()` raises `DiaspyError` when it cannot find user data,
* __rem__: `diaspy.client.Client` must be explicitly imported,
----
#### Version `0.3.1` (2013-07-12):
* __upd__: `diaspy.people.sephandle()` raises `InvalidHandleError` instead of `UserError`
* __upd__: `models.Post()._fetch()` renamed to `_fetchdata()` (because of new `_fetchcomments()` method)
* __new__: `models.Comment()` object: wrapper for comments, not to be created manually
* __new__: `comments` parameter in `models.Post`: defines whether to fetch post's commets
* __new__: `connection.Connection` has new parameter in `__init__()`: it's `schema`
* __new__: `author()` method in `models.Post()`
The new parameter in `connection.Connection` is useful when operating with handles.
As handle does not contain schema (`http`, `https`, etc.) `_setlogin()` would raise an
unhandled exception -- `requests.exceptions.MissingSchema`.
Now, however, `Connection` will catch the exception, add missing schema and try once more.
This parameter is provided to give programmers ability to manipulate it.
Also, now you can pass just `pod.example.com` as `pod` parameter. Less typing!
When it comes to posts, we are now able to fetch comments.
----
#### Version `0.3.0` (2013-07-07):
First edition of Changelog for `diaspy`.
Developers should update their code as version `0.3.0` may not be fully
backwards compatible depending on how the code is written.
If you always pass named arguments and do not rely on their order you can, at least in
theory, not worry about this update.
Version `0.3.0` introduces few new features, fixes several bugs and brings a bit of
redesign and refactoring od `diaspy`'s code.
diaspy-0.6.0/LICENSE 0000664 0000000 0000000 00000002073 13371050132 0014010 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2013, Moritz Kiefer
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.
diaspy-0.6.0/Makefile 0000664 0000000 0000000 00000000542 13371050132 0014442 0 ustar 00root root 0000000 0000000 .PHONY: style-check test
style-check:
flake8 --max-complexity 6 ./diaspy/
test:
python3 -m unittest --verbose --catch --failfast tests.py
test-python2:
python2 -m unittest --verbose --catch --failfast tests
clean:
rm -v ./{diaspy/,}*.pyc
rm -rv ./{diaspy/,}__pycache__/
install:
python setup.py install
rm -rvf dist/
rm -rvf diaspy.egg-info
diaspy-0.6.0/README.md 0000664 0000000 0000000 00000004240 13371050132 0014260 0 ustar 00root root 0000000 0000000 ## Unofficial Python interface for Diaspora\* social network
[](https://pypi.python.org/pypi/diaspy-api)
`diaspy` is a set of modules which form an Python interface to the API of
Disapora\* social network.
Test suite will cause problems when run with 2.x so testing should be done
using python3 interpreter.
Object oriented design of `diaspy` makes it easily reusable by other
developers who want to use only part of the interface and create derivative
works from it.
Developrs who don't like the design of `diaspy` and want to create something better
can use only `diaspy.connection.Connection()` object as it is capable of
doing everything. Other modules are just layers that provide easier access to
parts of the Diaspora\* API.
----
### Dependencies
List of software `diaspy` requires to run.
Versions used by maintainer are the ones available in stock Arch x86\_64 repositories.
**`python`**
Version: 3.3.3
[Website](https://www.python.org/)
**`python-requests`**
Version: 2.1.0
[Website](http://docs.python-requests.org/en/latest/)
**`python-dateutil`**
Version: >= 2.2
[Website](https://github.com/dateutil/dateutil)
*Optional:* **`python-beautifulsoup4`**
[Website](https://www.crummy.com/software/BeautifulSoup/)
----
#### Quick intro
#### 1. Posting text to your stream
You only need two objects to do this: `Stream()` and `Connection()`.
>>> import diaspy
>>> c = diaspy.connection.Connection(pod='https://pod.example.com',
... username='foo',
... password='bar')
>>> c.login()
>>> stream = diaspy.streams.Stream(c)
>>> stream.post('Your first post')
----
#### 2. Reference implementation
There is no official reference implementation of D\* client using `diaspy`.
The `diaspy.client` module is no longer maintained and will be removed in the future.
However, there is a small script written that uses `diaspy` as its backend.
Look for `diacli` in marekjm's repositories on GitHub.
----
To get more information about how the code works read
documentation (`./doc/` directory) and manual (`./manual/` directory).
diaspy-0.6.0/diaspy/ 0000775 0000000 0000000 00000000000 13371050132 0014272 5 ustar 00root root 0000000 0000000 diaspy-0.6.0/diaspy/__init__.py 0000664 0000000 0000000 00000000523 13371050132 0016403 0 ustar 00root root 0000000 0000000 # flake8: noqa
import diaspy.connection as connection
import diaspy.models as models
import diaspy.streams as streams
import diaspy.conversations as messages
import diaspy.conversations as conversations
import diaspy.people as people
import diaspy.notifications as notifications
import diaspy.settings as settings
__version__ = '0.6.0'
diaspy-0.6.0/diaspy/connection.py 0000664 0000000 0000000 00000017175 13371050132 0017016 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This module abstracts connection to pod.
"""
import json
import re
import requests
import warnings
from diaspy import errors
DEBUG = True
class Connection():
"""Object representing connection with the pod.
"""
_token_regex = re.compile(r'name="csrf-token"\s+content="(.*?)"')
_userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
# this is for older version of D*
_token_regex_2 = re.compile(r'content="(.*?)"\s+name="csrf-token')
_userinfo_regex_2 = re.compile(r'gon.user=({.*?});gon.')
_verify_SSL = True
def __init__(self, pod, username, password, schema='https'):
"""
:param pod: The complete url of the diaspora pod to use.
:type pod: str
:param username: The username used to log in.
:type username: str
:param password: The password used to log in.
:type password: str
"""
self.pod = pod
self._session = requests.Session()
self._login_data = {'user[remember_me]': 1, 'utf8': '✓'}
self._userdata = {}
self._token = ''
self._diaspora_session = ''
self._fetch_token_from = 'stream'
try: self._setlogin(username, password)
except requests.exceptions.MissingSchema:
self.pod = '{0}://{1}'.format(schema, self.pod)
warnings.warn('schema was missing')
try: self._setlogin(username, password)
except Exception as e:
raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
self._cookies = self._fetchcookies()
def _fetchcookies(self):
request = self.get('stream')
return request.cookies
def __repr__(self):
"""Returns token string.
It will be easier to change backend if programs will just use:
repr(connection)
instead of calling a specified method.
"""
return self._fetchtoken()
def get(self, string, headers={}, params={}, direct=False, **kwargs):
"""This method gets data from session.
Performs additional checks if needed.
Example:
To obtain 'foo' from pod one should call `get('foo')`.
:param string: URL to get without the pod's URL and slash eg. 'stream'.
:type string: str
:param direct: if passed as True it will not be expanded
:type direct: bool
"""
if not direct: url = '{0}/{1}'.format(self.pod, string)
else: url = string
return self._session.get(url, params=params, headers=headers, verify=self._verify_SSL, **kwargs)
def tokenFrom(self, location):
"""Sets location for the *next* fetch of CSRF token.
Intended to be used for oneliners like this one:
connection.tokenFrom('somewhere').delete(...)
where the token comes from "somewhere" instead of the
default stream page.
"""
self._fetch_token_from = location
return self
def post(self, string, data, headers={}, params={}, **kwargs):
"""This method posts data to session.
Performs additional checks if needed.
Example:
To post to 'foo' one should call `post('foo', data={})`.
:param string: URL to post without the pod's URL and slash eg. 'status_messages'.
:type string: str
:param data: Data to post.
:param headers: Headers (optional).
:type headers: dict
:param params: Parameters (optional).
:type params: dict
"""
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
request = self._session.post(string, data, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
def put(self, string, data=None, headers={}, params={}, **kwargs):
"""This method PUTs to session.
"""
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
if data is not None: request = self._session.put(string, data, headers=headers, params=params, **kwargs)
else: request = self._session.put(string, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
def delete(self, string, data = None, headers={}, **kwargs):
"""This method lets you send delete request to session.
Performs additional checks if needed.
:param string: URL to use.
:type string: str
:param data: Data to use.
:param headers: Headers to use (optional).
:type headers: dict
"""
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
request = self._session.delete(string, data=data, headers=headers, verify=self._verify_SSL, **kwargs)
return request
def _setlogin(self, username, password):
"""This function is used to set data for login.
.. note::
It should be called before _login() function.
"""
self._login_data = {'user[username]': username,
'user[password]': password,
'authenticity_token': self._fetchtoken()}
def _login(self):
"""Handles actual login request.
Raises LoginError if login failed.
"""
request = self.post('users/sign_in',
data=self._login_data,
allow_redirects=False)
if request.status_code != 302:
raise errors.LoginError('{0}: login failed'.format(request.status_code))
def login(self, remember_me=1):
"""This function is used to log in to a pod.
Will raise LoginError if password or username was not specified.
"""
if not self._login_data['user[username]'] or not self._login_data['user[password]']:
raise errors.LoginError('username and/or password is not specified')
self._login_data['user[remember_me]'] = remember_me
status = self._login()
self._login_data = {}
return self
def logout(self):
"""Logs out from a pod.
When logged out you can't do anything.
"""
self.get('users/sign_out')
self.token = ''
def podswitch(self, pod, username, password, login=True):
"""Switches pod from current to another one.
"""
self.pod = pod
self._setlogin(username, password)
if login: self._login()
def _fetchtoken(self):
"""This method tries to get token string needed for authentication on D*.
:returns: token string
"""
request = self.get(self._fetch_token_from)
token = self._token_regex.search(request.text)
if token is None: token = self._token_regex_2.search(request.text)
if token is not None: token = token.group(1)
else: raise errors.TokenError('could not find valid CSRF token')
self._token = token
self._fetch_token_from = 'stream'
return token
def get_token(self, fetch=True):
"""This function returns a token needed for authentication in most cases.
**Notice:** using repr() is recommended method for getting token.
Each time it is run a _fetchtoken() is called and refreshed token is stored.
It is more safe to use than _fetchtoken().
By setting new you can request new token or decide to get stored one.
If no token is stored new one will be fetched anyway.
:returns: string -- token used to authenticate
"""
try:
if fetch or not self._token: self._fetchtoken()
except requests.exceptions.ConnectionError as e:
warnings.warn('{0} was cought: reusing old token'.format(e))
finally:
if not self._token: raise errors.TokenError('cannot obtain token and no previous token found for reuse')
return self._token
def getSessionToken(self):
"""Returns session token string (_diaspora_session).
"""
return self._diaspora_session
def userdata(self):
return self._userdata
def getUserData(self):
"""Returns user data.
"""
request = self.get('bookmarklet')
userdata = self._userinfo_regex.search(request.text)
if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
if userdata is None: raise errors.DiaspyError('cannot find user data')
userdata = userdata.group(1)
self._userdata = json.loads(userdata)
return self._userdata
def set_verify_SSL(self, verify):
"""Sets whether there should be an error if a SSL-Certificate could not be verified.
"""
self._verify_SSL = verify
diaspy-0.6.0/diaspy/conversations.py 0000664 0000000 0000000 00000001422 13371050132 0017540 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from diaspy import errors, models
class Mailbox():
"""Object implementing diaspora* mailbox.
"""
def __init__(self, connection, fetch=True):
self._connection = connection
self._mailbox = []
if fetch: self._fetch()
def __len__(self):
return len(self._mailbox)
def __iter__(self):
return iter(self._mailbox)
def __getitem__(self, n):
return self._mailbox[n]
def _fetch(self):
"""This method will fetch messages from user's mailbox.
"""
request = self._connection.get('conversations.json')
if request.status_code != 200:
raise errors.DiaspyError('wrong status code: {0}'.format(request.status_code))
mailbox = request.json()
self._mailbox = [models.Conversation(self._connection, c['conversation']['id']) for c in mailbox]
diaspy-0.6.0/diaspy/errors.py 0000664 0000000 0000000 00000006754 13371050132 0016174 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""This module contains custom exceptions that are raised by diaspy.
These are not described by DIASPORA* protocol as exceptions that should be
raised by API implementations but are specific to this particular implementation.
If your program should catch all exceptions raised by diaspy and
does not need to handle them specifically you can use following code:
# this line imports all errors
from diaspy.errors import *
try:
# your code...
except DiaspyError as e:
# your error handling code...
finally:
# closing code...
"""
import warnings
class DiaspyError(Exception):
"""Base exception for all errors
raised by diaspy.
"""
pass
class LoginError(DiaspyError):
"""Exception raised when something
bad happens while performing actions
related to logging in.
"""
pass
class TokenError(DiaspyError):
pass
class CSRFProtectionKickedIn(TokenError):
pass
class DataError(DiaspyError):
pass
class InvalidDataError(DataError):
pass
class KeyMissingFromFetchedData(InvalidDataError):
pass
class UserError(DiaspyError):
"""Exception raised when something related to users goes wrong.
"""
pass
class InvalidHandleError(DiaspyError):
"""Raised when invalid handle is found.
"""
pass
class SearchError(DiaspyError):
"""Exception raised when something related to search goes wrong.
"""
pass
class ConversationError(DiaspyError):
"""Exception raised when something related to conversations goes wrong.
"""
pass
class AspectError(DiaspyError):
"""Exception raised when something related to aspects goes wrong.
"""
pass
class UserIsNotMemberOfAspect(AspectError):
pass
class PostError(DiaspyError):
"""Exception raised when something related to posts goes wrong.
"""
pass
class StreamError(DiaspyError):
"""Exception raised when something related to streams goes wrong.
"""
pass
class SettingsError(DiaspyError):
"""Exception raised when something related to settings goes wrong.
"""
pass
class SearchError(DiaspyError):
"""Exception raised when something related to searching goes wrong.
"""
pass
class TagError(DiaspyError):
"""Exception raised when something related to a tag goes wrong.
"""
pass
def react(r, message='', accepted=[200, 201, 202, 203, 204, 205, 206], exception=DiaspyError):
"""This method tries to decide how to react
to a response code passed to it. If it's an
error code it will raise an exception (it will
call `throw()` method.
If response code is not accepted AND cannot
be matched to any exception, generic exception
(DiaspyError) is raised (provided that `exception`
param was left untouched).
By default `accepted` param contains all HTTP
success codes.
User can force type of exception to raise by passing
`exception` param.
:param r: response code
:type r: int
:param message: message for the exception
:type message: str
:param accepted: list of accepted error codes
:type accepted: list
:param exception: preferred exception to raise
:type exception: valid exception type (default: DiaspyError)
"""
warnings.warn(DeprecationWarning)
if r in accepted: e = None
else: e = DiaspyError
if e is not None: e = exception
throw(e, message=message)
def throw(e, message=''):
"""This function throws an error with given message.
If None is passed as `e` throw() will not raise
anything.
:param e: exception to throw
:type e: any valid exception type or None
:param message: message for exception
:type message: str
"""
warnings.warn(DeprecationWarning)
if e is None: pass
else: raise e(message)
diaspy-0.6.0/diaspy/models.py 0000664 0000000 0000000 00000055142 13371050132 0016136 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""This module is only imported in other diaspy modules and
MUST NOT import anything.
"""
import json
import copy
BS4_SUPPORT=False
try:
from bs4 import BeautifulSoup
except ImportError:
import re
print("[diaspy] BeautifulSoup not found, falling back on regex.")
else: BS4_SUPPORT=True
from diaspy import errors
class Aspect():
"""This class represents an aspect.
Class can be initialized by passing either an id and/or name as
parameters.
If both are missing, an exception will be raised.
"""
def __init__(self, connection, id, name=None):
self._connection = connection
self.id, self.name = id, name
self._cached = []
def getUsers(self, fetch = True):
"""Returns list of GUIDs of users who are listed in this aspect.
"""
if fetch:
request = self._connection.get('contacts.json?a_id={}'.format(self.id))
self._cached = request.json()
return self._cached
def removeAspect(self):
"""
--> POST /aspects/{id} HTTP/1.1
--> _method=delete&authenticity_token={token}
<-- HTTP/1.1 302 Found
Removes whole aspect.
:returns: None
"""
request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format(self.id))
if request.status_code != 302:
raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
def addUser(self, user_id):
"""Add user to current aspect.
:param user_id: user to add to aspect
:type user_id: int
:returns: JSON from request
--> POST /aspect_memberships HTTP/1.1
--> Accept: application/json, text/javascript, */*; q=0.01
--> Content-Type: application/json; charset=UTF-8
--> {"aspect_id":123,"person_id":123}
<-- HTTP/1.1 200 OK
"""
data = {'aspect_id': self.id,
'person_id': user_id}
headers = {'content-type': 'application/json',
'accept': 'application/json'}
request = self._connection.tokenFrom('contacts').post('aspect_memberships', data=json.dumps(data), headers=headers)
if request.status_code == 400:
raise errors.AspectError('duplicate record, user already exists in aspect: {0}'.format(request.status_code))
elif request.status_code == 404:
raise errors.AspectError('user not found from this pod: {0}'.format(request.status_code))
elif request.status_code != 200:
raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
response = None
try:
response = request.json()
except json.decoder.JSONDecodeError:
""" Should be OK now, but I'll leave this commentary here
at first to see if anything comes up """
# FIXME For some (?) reason removing users from aspects works, but
# adding them is a no-go and Diaspora* kicks us out with CSRF errors.
# Weird.
pass
if response is None:
raise errors.CSRFProtectionKickedIn()
# Now you should fetchguid(fetch_stream=False) on User to update aspect membership_id's
# Or update it locally with the response
return response
def removeUser(self, user):
"""Remove user from current aspect.
:param user: user to remove from aspect
:type user: diaspy.people.User object
"""
membership_id = None
to_remove = None
for each in user.aspectMemberships():
if each.get('aspect', {}).get('id') == self.id:
membership_id = each.get('id')
to_remove = each
break # no need to continue
if membership_id is None:
raise errors.UserIsNotMemberOfAspect(user, self)
request = self._connection.delete('aspect_memberships/{0}'.format(membership_id))
if request.status_code == 404:
raise errors.AspectError('cannot remove user from aspect, probably tried too fast after adding: {0}'.format(request.status_code))
elif request.status_code != 200:
raise errors.AspectError('cannot remove user from aspect: {0}'.format(request.status_code))
if 'contact' in user.data: # User object
if to_remove: user.data['contact']['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
else: # User object from Contacts()
if to_remove: user.data['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
return request.json()
class Notification():
"""This class represents single notification.
"""
if not BS4_SUPPORT:
_who_regexp = re.compile(r'/people/([0-9a-f]+)["\']{1} class=["\']{1}hovercardable')
_aboutid_regexp = re.compile(r'/posts/[0-9a-f]+')
_htmltag_regexp = re.compile('?[a-z]+( *[a-z_-]+=["\'].*?["\'])* */?>')
def __init__(self, connection, data):
self._connection = connection
self.type = data['type']
self._data = data[self.type]
self.id = self._data['id']
self.unread = self._data['unread']
def __getitem__(self, key):
"""Returns a key from notification data.
"""
return self._data[key]
def __str__(self):
"""Returns notification note.
"""
if BS4_SUPPORT:
soup = BeautifulSoup(self._data['note_html'], 'lxml')
media_body = soup.find('div', {"class": "media-body"})
div = media_body.find('div')
if div: div.decompose()
return media_body.getText().strip()
else:
string = re.sub(self._htmltag_regexp, '', self._data['note_html'])
string = string.strip().split('\n')[0]
while ' ' in string: string = string.replace(' ', ' ')
return string
def __repr__(self):
"""Returns notification note with more details.
"""
return '{0}: {1}'.format(self.when(), str(self))
def about(self):
"""Returns id of post about which the notification is informing OR:
If the id is None it means that it's about user so .who() is called.
"""
if BS4_SUPPORT:
soup = BeautifulSoup(self._data['note_html'], 'lxml')
id = soup.find('a', {"data-ref": True})
if id: return id['data-ref']
else: return self.who()[0]
else:
about = self._aboutid_regexp.search(self._data['note_html'])
if about is None: about = self.who()[0]
else: about = int(about.group(0)[7:])
return about
def who(self):
"""Returns list of guids of the users who caused you to get the notification.
"""
if BS4_SUPPORT: # Parse the HTML with BS4
soup = BeautifulSoup(self._data['note_html'], 'lxml')
hovercardable_soup = soup.findAll('a', {"class": "hovercardable"})
return list(set([soup['href'][8:] for soup in hovercardable_soup]))
else:
return list(set([who for who in self._who_regexp.findall(self._data['note_html'])]))
def when(self):
"""Returns UTC time as found in note_html.
"""
return self._data['created_at']
def mark(self, unread=False):
"""Marks notification to read/unread.
Marks notification to read if `unread` is False.
Marks notification to unread if `unread` is True.
:param unread: which state set for notification
:type unread: bool
"""
headers = {'x-csrf-token': repr(self._connection)}
params = {'set_unread': json.dumps(unread)}
self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
self._data['unread'] = unread
class Conversation():
"""This class represents a conversation.
.. note::
Remember that you need to have access to the conversation.
"""
if not BS4_SUPPORT:
_message_stream_regexp = re.compile(r'