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._cookies = self._fetchcookies()
- self._fetch_token_from = 'stream'
- try:
- #self._setlogin(username, password)
- self._login_data = {'user[username]': username,
- 'user[password]': password,
- 'authenticity_token': self._fetchtoken()}
- success = True
- except requests.exceptions.MissingSchema:
- self.pod = '{0}://{1}'.format(schema, self.pod)
- warnings.warn('schema was missing')
- success = False
- finally:
- pass
- try:
- if not success:
- self._login_data = {'user[username]': username,
- 'user[password]': password,
- 'authenticity_token': self._fetchtoken()}
- except Exception as e:
- raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
-
- 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):
- """Switches pod from current to another one.
- """
- self.pod = pod
- self._setlogin(username, password)
- 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
+ """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._cookies = self._fetchcookies()
+ self._fetch_token_from = 'stream'
+ try:
+ #self._setlogin(username, password)
+ self._login_data = {'user[username]': username,
+ 'user[password]': password,
+ 'authenticity_token': self._fetchtoken()}
+ success = True
+ except requests.exceptions.MissingSchema:
+ self.pod = '{0}://{1}'.format(schema, self.pod)
+ warnings.warn('schema was missing')
+ success = False
+ finally:
+ pass
+ try:
+ if not success:
+ self._login_data = {'user[username]': username,
+ 'user[password]': password,
+ 'authenticity_token': self._fetchtoken()}
+ except Exception as e:
+ raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
+
+ 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):
+ """Switches pod from current to another one.
+ """
+ self.pod = pod
+ self._setlogin(username, password)
+ 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
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]
+ """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]
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...
+ # 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
+ """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
+ """Exception raised when something
+ bad happens while performing actions
+ related to logging in.
+ """
+ pass
class TokenError(DiaspyError):
- pass
+ pass
class CSRFProtectionKickedIn(TokenError):
- pass
+ pass
class DataError(DiaspyError):
- pass
+ pass
class InvalidDataError(DataError):
- pass
+ pass
class KeyMissingFromFetchedData(InvalidDataError):
- pass
+ pass
class UserError(DiaspyError):
- """Exception raised when something related to users goes wrong.
- """
- pass
+ """Exception raised when something related to users goes wrong.
+ """
+ pass
class InvalidHandleError(DiaspyError):
- """Raised when invalid handle is found.
- """
- pass
+ """Raised when invalid handle is found.
+ """
+ pass
class SearchError(DiaspyError):
- """Exception raised when something related to search goes wrong.
- """
- pass
+ """Exception raised when something related to search goes wrong.
+ """
+ pass
class ConversationError(DiaspyError):
- """Exception raised when something related to conversations goes wrong.
- """
- pass
+ """Exception raised when something related to conversations goes wrong.
+ """
+ pass
class AspectError(DiaspyError):
- """Exception raised when something related to aspects goes wrong.
- """
- pass
+ """Exception raised when something related to aspects goes wrong.
+ """
+ pass
class UserIsNotMemberOfAspect(AspectError):
- pass
+ pass
class PostError(DiaspyError):
- """Exception raised when something related to posts goes wrong.
- """
- pass
+ """Exception raised when something related to posts goes wrong.
+ """
+ pass
class StreamError(DiaspyError):
- """Exception raised when something related to streams goes wrong.
- """
- pass
+ """Exception raised when something related to streams goes wrong.
+ """
+ pass
class SettingsError(DiaspyError):
- """Exception raised when something related to settings goes wrong.
- """
- pass
+ """Exception raised when something related to settings goes wrong.
+ """
+ pass
class SearchError(DiaspyError):
- """Exception raised when something related to searching goes wrong.
- """
- pass
+ """Exception raised when something related to searching goes wrong.
+ """
+ pass
class TagError(DiaspyError):
- """Exception raised when something related to settings goes wrong.
- """
- pass
+ """Exception raised when something related to settings 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)
+ """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)
+ """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)
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
-
- TODO: status_codes
-
- 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():
- print(self.id, each)
- 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()
+ """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
+
+ TODO: status_codes
+
+ 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():
+ print(self.id, each)
+ 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.
- """
- _who_regexp = re.compile(r'/people/([0-9a-f]+)["\']{1} class=["\']{1}hovercardable')
- _when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
- _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.
- """
- 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.
- """
- about = self._aboutid_regexp.search(self._data['note_html'])
- if about is None: about = self.who()
- 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.
- """
- return [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
+ """This class represents single notification.
+ """
+ _who_regexp = re.compile(r'/people/([0-9a-f]+)["\']{1} class=["\']{1}hovercardable')
+ _when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
+ _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.
+ """
+ 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.
+ """
+ about = self._aboutid_regexp.search(self._data['note_html'])
+ if about is None: about = self.who()
+ 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.
+ """
+ return [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.
- """
- def __init__(self, connection, id, fetch=True):
- """
- :param conv_id: id of the post and not the guid!
- :type conv_id: str
- :param connection: connection object used to authenticate
- :type connection: connection.Connection
- """
- self._connection = connection
- self.id = id
- self._data = {}
- if fetch: self._fetch()
-
- def _fetch(self):
- """Fetches JSON data representing conversation.
- """
- request = self._connection.get('conversations/{}.json'.format(self.id))
- if request.status_code == 200:
- self._data = request.json()['conversation']
- else:
- raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
-
- def answer(self, text):
- """Answer that conversation
-
- :param text: text to answer.
- :type text: str
- """
- data = {'message[text]': text,
- 'utf8': '✓',
- 'authenticity_token': repr(self._connection)}
-
- request = self._connection.post('conversations/{}/messages'.format(self.id),
- data=data,
- headers={'accept': 'application/json'})
- if request.status_code != 200:
- raise errors.ConversationError('{0}: Answer could not be posted.'
- .format(request.status_code))
- return request.json()
-
- def delete(self):
- """Delete this conversation.
- Has to be implemented.
- """
- data = {'authenticity_token': repr(self._connection)}
-
- request = self._connection.delete('conversations/{0}/visibility/'
- .format(self.id),
- data=data,
- headers={'accept': 'application/json'})
-
- if request.status_code != 404:
- raise errors.ConversationError('{0}: Conversation could not be deleted.'
- .format(request.status_code))
-
- def get_subject(self):
- """Returns the subject of this conversation
- """
- return self._data['subject']
+ """This class represents a conversation.
+
+ .. note::
+ Remember that you need to have access to the conversation.
+ """
+ def __init__(self, connection, id, fetch=True):
+ """
+ :param conv_id: id of the post and not the guid!
+ :type conv_id: str
+ :param connection: connection object used to authenticate
+ :type connection: connection.Connection
+ """
+ self._connection = connection
+ self.id = id
+ self._data = {}
+ if fetch: self._fetch()
+
+ def _fetch(self):
+ """Fetches JSON data representing conversation.
+ """
+ request = self._connection.get('conversations/{}.json'.format(self.id))
+ if request.status_code == 200:
+ self._data = request.json()['conversation']
+ else:
+ raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
+
+ def answer(self, text):
+ """Answer that conversation
+
+ :param text: text to answer.
+ :type text: str
+ """
+ data = {'message[text]': text,
+ 'utf8': '✓',
+ 'authenticity_token': repr(self._connection)}
+
+ request = self._connection.post('conversations/{}/messages'.format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+ if request.status_code != 200:
+ raise errors.ConversationError('{0}: Answer could not be posted.'
+ .format(request.status_code))
+ return request.json()
+
+ def delete(self):
+ """Delete this conversation.
+ Has to be implemented.
+ """
+ data = {'authenticity_token': repr(self._connection)}
+
+ request = self._connection.delete('conversations/{0}/visibility/'
+ .format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+
+ if request.status_code != 404:
+ raise errors.ConversationError('{0}: Conversation could not be deleted.'
+ .format(request.status_code))
+
+ def get_subject(self):
+ """Returns the subject of this conversation
+ """
+ return self._data['subject']
class Comment():
- """Represents comment on post.
-
- Does not require Connection() object. Note that you should not manually
- create `Comment()` objects -- they are designed to be created automatically
- by `Post()` objects.
- """
- def __init__(self, data):
- self._data = data
- self.id = data['id']
- self.guid = data['guid']
-
- def __str__(self):
- """Returns comment's text.
- """
- return self._data['text']
-
- def __repr__(self):
- """Returns comments text and author.
- Format: AUTHOR (AUTHOR'S GUID): COMMENT
- """
- return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
-
- def when(self):
- """Returns time when the comment had been created.
- """
- return self._data['created_at']
-
- def author(self, key='name'):
- """Returns author of the comment.
- """
- return self._data['author'][key]
+ """Represents comment on post.
+
+ Does not require Connection() object. Note that you should not manually
+ create `Comment()` objects -- they are designed to be created automatically
+ by `Post()` objects.
+ """
+ def __init__(self, data):
+ self._data = data
+ self.id = data['id']
+ self.guid = data['guid']
+
+ def __str__(self):
+ """Returns comment's text.
+ """
+ return self._data['text']
+
+ def __repr__(self):
+ """Returns comments text and author.
+ Format: AUTHOR (AUTHOR'S GUID): COMMENT
+ """
+ return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
+
+ def when(self):
+ """Returns time when the comment had been created.
+ """
+ return self._data['created_at']
+
+ def author(self, key='name'):
+ """Returns author of the comment.
+ """
+ return self._data['author'][key]
class Comments():
- def __init__(self, comments=None):
- self._comments = comments
-
- def __iter__(self):
- if self._comments:
- for comment in self._comments:
- yield comment
-
- def __len__(self):
- if self._comments:
- return len(self._comments)
-
- def __getitem__(self, index):
- if self._comments:
- return self._comments[index]
-
- def __bool__(self):
- if self._comments:
- return True
- return False
-
- def ids(self):
- return [c.id for c in self._comments]
-
- def add(self, comment):
- """ Expects comment object
- TODO self._comments is None sometimes, have to look into it."""
- if comment and self._comments:
- self._comments.append(comment)
-
- def set(self, comments):
- """Sets comments wich already have a Comment obj"""
- if comments:
- self._comments = comments
-
- def set_json(self, json_comments):
- """Sets comments for this post from post data."""
- if json_comments:
- self._comments = [Comment(c) for c in json_comments]
+ def __init__(self, comments=None):
+ self._comments = comments
+
+ def __iter__(self):
+ if self._comments:
+ for comment in self._comments:
+ yield comment
+
+ def __len__(self):
+ if self._comments:
+ return len(self._comments)
+
+ def __getitem__(self, index):
+ if self._comments:
+ return self._comments[index]
+
+ def __bool__(self):
+ if self._comments:
+ return True
+ return False
+
+ def ids(self):
+ return [c.id for c in self._comments]
+
+ def add(self, comment):
+ """ Expects comment object
+ TODO self._comments is None sometimes, have to look into it."""
+ if comment and self._comments:
+ self._comments.append(comment)
+
+ def set(self, comments):
+ """Sets comments wich already have a Comment obj"""
+ if comments:
+ self._comments = comments
+
+ def set_json(self, json_comments):
+ """Sets comments for this post from post data."""
+ if json_comments:
+ self._comments = [Comment(c) for c in json_comments]
class Post():
- """This class represents a post.
-
- .. note::
- Remember that you need to have access to the post.
- """
- def __init__(self, connection, id=0, guid='', fetch=True, comments=True, post_data=None):
- """
- :param id: id of the post (GUID is recommended)
- :type id: int
- :param guid: GUID of the post
- :type guid: str
- :param connection: connection object used to authenticate
- :type connection: connection.Connection
- :param fetch: defines whether to fetch post's data or not
- :type fetch: bool
- :param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
- :type comments: bool
- :param post_data: contains post data so no need to fetch the post if this is set, until you want to update post data
- :type: json
- """
- if not (guid or id): raise TypeError('neither guid nor id was provided')
- self._connection = connection
- self.id = id
- self.guid = guid
- self._data = {}
- self.comments = Comments()
- if post_data:
- self._data = post_data
-
- if fetch: self._fetchdata()
- if comments:
- if not self._data: self._fetchdata()
- self._fetchcomments()
- else:
- if not self._data: self._fetchdata()
- self.comments.set_json( self['interactions']['comments'] )
-
- def __repr__(self):
- """Returns string containing more information then str().
- """
- return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
-
- def __str__(self):
- """Returns text of a post.
- """
- return self._data['text']
-
- def __getitem__(self, key):
- """FIXME This is deprecated, use diaspy.models.Post.data() instead to access
- data of Post objects.
- """
- return self._data[key]
-
- def __dict__(self):
- """Returns dictionary of posts data.
- FIXME This is deprecated, use diaspy.models.Post.data() instead.
- """
- return self._data
-
- def _fetchdata(self):
- """This function retrieves data of the post.
-
- :returns: guid of post whose data was fetched
- """
- if self.id: id = self.id
- if self.guid: id = self.guid
- request = self._connection.get('posts/{0}.json'.format(id))
- if request.status_code != 200:
- raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
- elif request:
- self._data = request.json()
- return self['guid']
-
- def _fetchcomments(self):
- """Retreives comments for this post.
- Retrieving comments via GUID will result in 404 error.
- DIASPORA* does not supply comments through /posts/:guid/ endpoint.
- """
- id = self._data['id']
- if self['interactions']['comments_count']:
- request = self._connection.get('posts/{0}/comments.json'.format(id))
- if request.status_code != 200:
- raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
- else:
- self.comments.set([Comment(c) for c in request.json()])
-
- def update(self):
- """Updates post data.
- FIXME This is deprecated.
- """
- print('diaspy.models.Post.update() is deprecated. Use diaspy.models.Post.update() instead.')
- self._fetchdata()
- self._fetchcomments()
-
- def fetch(self, comments = False):
- """Fetches post data.
- Use this function instead of diaspy.models.Post.update().
- """
- self._fetchdata()
- if comments:
- self._fetchcomments()
- return self
-
- def data(self, data = None):
- if data is not None:
- self._data = data
- return self._data
-
- def like(self):
- """This function likes a post.
- It abstracts the 'Like' functionality.
-
- :returns: dict -- json formatted like object.
- """
- data = {'authenticity_token': repr(self._connection)}
-
- request = self._connection.post('posts/{0}/likes'.format(self.id),
- data=data,
- headers={'accept': 'application/json'})
-
- if request.status_code != 201:
- raise errors.PostError('{0}: Post could not be liked.'
- .format(request.status_code))
-
- likes_json = request.json()
- if likes_json:
- self._data['interactions']['likes'] = [likes_json]
- return likes_json
-
- def reshare(self):
- """This function reshares a post
- """
- data = {'root_guid': self._data['guid'],
- 'authenticity_token': repr(self._connection)}
-
- request = self._connection.post('reshares',
- data=data,
- headers={'accept': 'application/json'})
- if request.status_code != 201:
- raise Exception('{0}: Post could not be reshared'.format(request.status_code))
- return request.json()
-
- def comment(self, text):
- """This function comments on a post
-
- :param text: text to comment.
- :type text: str
- """
- data = {'text': text,
- 'authenticity_token': repr(self._connection)}
- request = self._connection.post('posts/{0}/comments'.format(self.id),
- data=data,
- headers={'accept': 'application/json'})
-
- if request.status_code != 201:
- raise Exception('{0}: Comment could not be posted.'
- .format(request.status_code))
- return Comment(request.json())
-
- def vote_poll(self, poll_answer_id):
- """This function votes on a post's poll
-
- :param poll_answer_id: id to poll vote.
- :type poll_answer_id: int
- """
- poll_id = self._data['poll']['poll_id']
- data = {'poll_answer_id': poll_answer_id,
- 'poll_id': poll_id,
- 'post_id': self.id,
- 'authenticity_token': repr(self._connection)}
- request = self._connection.post('posts/{0}/poll_participations'.format(self.id),
- data=data,
- headers={'accept': 'application/json'})
- if request.status_code != 201:
- raise Exception('{0}: Vote on poll failed.'
- .format(request.status_code))
- return request.json()
-
- def hide(self):
- """
- -> PUT /share_visibilities/42 HTTP/1.1
- post_id=123
- <- HTTP/1.1 200 OK
- """
- headers = {'x-csrf-token': repr(self._connection)}
- params = {'post_id': json.dumps(self.id)}
- request = self._connection.put('share_visibilities/42', params=params, headers=headers)
- if request.status_code != 200:
- raise Exception('{0}: Failed to hide post.'
- .format(request.status_code))
-
- def mute(self):
- """
- -> POST /blocks HTTP/1.1
- {"block":{"person_id":123}}
- <- HTTP/1.1 204 No Content
- """
- headers = {'content-type':'application/json', 'x-csrf-token': repr(self._connection)}
- data = json.dumps({ 'block': { 'person_id' : self._data['author']['id'] } })
- request = self._connection.post('blocks', data=data, headers=headers)
- if request.status_code != 204:
- raise Exception('{0}: Failed to block person'
- .format(request.status_code))
-
- def subscribe(self):
- """
- -> POST /posts/123/participation HTTP/1.1
- <- HTTP/1.1 201 Created
- """
- headers = {'x-csrf-token': repr(self._connection)}
- data = {}
- request = self._connection.post('posts/{}/participation'
- .format( self.id ), data=data, headers=headers)
- if request.status_code != 201:
- raise Exception('{0}: Failed to subscribe to post'
- .format(request.status_code))
-
- def unsubscribe(self):
- """
- -> POST /posts/123/participation HTTP/1.1
- _method=delete
- <- HTTP/1.1 200 OK
- """
- headers = {'x-csrf-token': repr(self._connection)}
- data = { "_method": "delete" }
- request = self._connection.post('posts/{}/participation'
- .format( self.id ), headers=headers, data=data)
- if request.status_code != 200:
- raise Exception('{0}: Failed to unsubscribe to post'
- .format(request.status_code))
-
- def report(self):
- """
- TODO
- """
- pass
-
- def delete(self):
- """ This function deletes this post
- """
- data = {'authenticity_token': repr(self._connection)}
- request = self._connection.delete('posts/{0}'.format(self.id),
- data=data,
- headers={'accept': 'application/json'})
- if request.status_code != 204:
- raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
-
- def delete_comment(self, comment_id):
- """This function removes a comment from a post
-
- :param comment_id: id of the comment to remove.
- :type comment_id: str
- """
- data = {'authenticity_token': repr(self._connection)}
- request = self._connection.delete('posts/{0}/comments/{1}'
- .format(self.id, comment_id),
- data=data,
- headers={'accept': 'application/json'})
-
- if request.status_code != 204:
- raise errors.PostError('{0}: Comment could not be deleted'
- .format(request.status_code))
-
- def delete_like(self):
- """This function removes a like from a post
- """
- data = {'authenticity_token': repr(self._connection)}
- url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
- request = self._connection.delete(url, data=data)
- if request.status_code != 204:
- raise errors.PostError('{0}: Like could not be removed.'
- .format(request.status_code))
-
- def author(self, key='name'):
- """Returns author of the post.
- :param key: all keys available in data['author']
- """
- return self._data['author'][key]
+ """This class represents a post.
+
+ .. note::
+ Remember that you need to have access to the post.
+ """
+ def __init__(self, connection, id=0, guid='', fetch=True, comments=True, post_data=None):
+ """
+ :param id: id of the post (GUID is recommended)
+ :type id: int
+ :param guid: GUID of the post
+ :type guid: str
+ :param connection: connection object used to authenticate
+ :type connection: connection.Connection
+ :param fetch: defines whether to fetch post's data or not
+ :type fetch: bool
+ :param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
+ :type comments: bool
+ :param post_data: contains post data so no need to fetch the post if this is set, until you want to update post data
+ :type: json
+ """
+ if not (guid or id): raise TypeError('neither guid nor id was provided')
+ self._connection = connection
+ self.id = id
+ self.guid = guid
+ self._data = {}
+ self.comments = Comments()
+ if post_data:
+ self._data = post_data
+
+ if fetch: self._fetchdata()
+ if comments:
+ if not self._data: self._fetchdata()
+ self._fetchcomments()
+ else:
+ if not self._data: self._fetchdata()
+ self.comments.set_json( self['interactions']['comments'] )
+
+ def __repr__(self):
+ """Returns string containing more information then str().
+ """
+ return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
+
+ def __str__(self):
+ """Returns text of a post.
+ """
+ return self._data['text']
+
+ def __getitem__(self, key):
+ """FIXME This is deprecated, use diaspy.models.Post.data() instead to access
+ data of Post objects.
+ """
+ return self._data[key]
+
+ def __dict__(self):
+ """Returns dictionary of posts data.
+ FIXME This is deprecated, use diaspy.models.Post.data() instead.
+ """
+ return self._data
+
+ def _fetchdata(self):
+ """This function retrieves data of the post.
+
+ :returns: guid of post whose data was fetched
+ """
+ if self.id: id = self.id
+ if self.guid: id = self.guid
+ request = self._connection.get('posts/{0}.json'.format(id))
+ if request.status_code != 200:
+ raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
+ elif request:
+ self._data = request.json()
+ return self['guid']
+
+ def _fetchcomments(self):
+ """Retreives comments for this post.
+ Retrieving comments via GUID will result in 404 error.
+ DIASPORA* does not supply comments through /posts/:guid/ endpoint.
+ """
+ id = self._data['id']
+ if self['interactions']['comments_count']:
+ request = self._connection.get('posts/{0}/comments.json'.format(id))
+ if request.status_code != 200:
+ raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
+ else:
+ self.comments.set([Comment(c) for c in request.json()])
+
+ def update(self):
+ """Updates post data.
+ FIXME This is deprecated.
+ """
+ print('diaspy.models.Post.update() is deprecated. Use diaspy.models.Post.update() instead.')
+ self._fetchdata()
+ self._fetchcomments()
+
+ def fetch(self, comments = False):
+ """Fetches post data.
+ Use this function instead of diaspy.models.Post.update().
+ """
+ self._fetchdata()
+ if comments:
+ self._fetchcomments()
+ return self
+
+ def data(self, data = None):
+ if data is not None:
+ self._data = data
+ return self._data
+
+ def like(self):
+ """This function likes a post.
+ It abstracts the 'Like' functionality.
+
+ :returns: dict -- json formatted like object.
+ """
+ data = {'authenticity_token': repr(self._connection)}
+
+ request = self._connection.post('posts/{0}/likes'.format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+
+ if request.status_code != 201:
+ raise errors.PostError('{0}: Post could not be liked.'
+ .format(request.status_code))
+
+ likes_json = request.json()
+ if likes_json:
+ self._data['interactions']['likes'] = [likes_json]
+ return likes_json
+
+ def reshare(self):
+ """This function reshares a post
+ """
+ data = {'root_guid': self._data['guid'],
+ 'authenticity_token': repr(self._connection)}
+
+ request = self._connection.post('reshares',
+ data=data,
+ headers={'accept': 'application/json'})
+ if request.status_code != 201:
+ raise Exception('{0}: Post could not be reshared'.format(request.status_code))
+ return request.json()
+
+ def comment(self, text):
+ """This function comments on a post
+
+ :param text: text to comment.
+ :type text: str
+ """
+ data = {'text': text,
+ 'authenticity_token': repr(self._connection)}
+ request = self._connection.post('posts/{0}/comments'.format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+
+ if request.status_code != 201:
+ raise Exception('{0}: Comment could not be posted.'
+ .format(request.status_code))
+ return Comment(request.json())
+
+ def vote_poll(self, poll_answer_id):
+ """This function votes on a post's poll
+
+ :param poll_answer_id: id to poll vote.
+ :type poll_answer_id: int
+ """
+ poll_id = self._data['poll']['poll_id']
+ data = {'poll_answer_id': poll_answer_id,
+ 'poll_id': poll_id,
+ 'post_id': self.id,
+ 'authenticity_token': repr(self._connection)}
+ request = self._connection.post('posts/{0}/poll_participations'.format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+ if request.status_code != 201:
+ raise Exception('{0}: Vote on poll failed.'
+ .format(request.status_code))
+ return request.json()
+
+ def hide(self):
+ """
+ -> PUT /share_visibilities/42 HTTP/1.1
+ post_id=123
+ <- HTTP/1.1 200 OK
+ """
+ headers = {'x-csrf-token': repr(self._connection)}
+ params = {'post_id': json.dumps(self.id)}
+ request = self._connection.put('share_visibilities/42', params=params, headers=headers)
+ if request.status_code != 200:
+ raise Exception('{0}: Failed to hide post.'
+ .format(request.status_code))
+
+ def mute(self):
+ """
+ -> POST /blocks HTTP/1.1
+ {"block":{"person_id":123}}
+ <- HTTP/1.1 204 No Content
+ """
+ headers = {'content-type':'application/json', 'x-csrf-token': repr(self._connection)}
+ data = json.dumps({ 'block': { 'person_id' : self._data['author']['id'] } })
+ request = self._connection.post('blocks', data=data, headers=headers)
+ if request.status_code != 204:
+ raise Exception('{0}: Failed to block person'
+ .format(request.status_code))
+
+ def subscribe(self):
+ """
+ -> POST /posts/123/participation HTTP/1.1
+ <- HTTP/1.1 201 Created
+ """
+ headers = {'x-csrf-token': repr(self._connection)}
+ data = {}
+ request = self._connection.post('posts/{}/participation'
+ .format( self.id ), data=data, headers=headers)
+ if request.status_code != 201:
+ raise Exception('{0}: Failed to subscribe to post'
+ .format(request.status_code))
+
+ def unsubscribe(self):
+ """
+ -> POST /posts/123/participation HTTP/1.1
+ _method=delete
+ <- HTTP/1.1 200 OK
+ """
+ headers = {'x-csrf-token': repr(self._connection)}
+ data = { "_method": "delete" }
+ request = self._connection.post('posts/{}/participation'
+ .format( self.id ), headers=headers, data=data)
+ if request.status_code != 200:
+ raise Exception('{0}: Failed to unsubscribe to post'
+ .format(request.status_code))
+
+ def report(self):
+ """
+ TODO
+ """
+ pass
+
+ def delete(self):
+ """ This function deletes this post
+ """
+ data = {'authenticity_token': repr(self._connection)}
+ request = self._connection.delete('posts/{0}'.format(self.id),
+ data=data,
+ headers={'accept': 'application/json'})
+ if request.status_code != 204:
+ raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
+
+ def delete_comment(self, comment_id):
+ """This function removes a comment from a post
+
+ :param comment_id: id of the comment to remove.
+ :type comment_id: str
+ """
+ data = {'authenticity_token': repr(self._connection)}
+ request = self._connection.delete('posts/{0}/comments/{1}'
+ .format(self.id, comment_id),
+ data=data,
+ headers={'accept': 'application/json'})
+
+ if request.status_code != 204:
+ raise errors.PostError('{0}: Comment could not be deleted'
+ .format(request.status_code))
+
+ def delete_like(self):
+ """This function removes a like from a post
+ """
+ data = {'authenticity_token': repr(self._connection)}
+ url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
+ request = self._connection.delete(url, data=data)
+ if request.status_code != 204:
+ raise errors.PostError('{0}: Like could not be removed.'
+ .format(request.status_code))
+
+ def author(self, key='name'):
+ """Returns author of the post.
+ :param key: all keys available in data['author']
+ """
+ return self._data['author'][key]
class Notifications():
- """This class represents notifications of a user.
- """
- def __init__(self, connection):
- self._connection = connection
- self._data = {}
- self._notifications = self.get()
- self.page = 1
-
- def __iter__(self):
- return iter(self._notifications)
-
- def __getitem__(self, n):
- return self._notifications[n]
-
- def _finalise(self, notifications):
- self._data['unread_count'] = notifications['unread_count']
- self._data['unread_count_by_type'] = notifications['unread_count_by_type']
- return [Notification(self._connection, n) for n in notifications.get('notification_list', [])]
-
- def last(self):
- """Returns list of most recent notifications.
- """
- params = {'per_page': 5, '_': int(round(time.time(), 3)*1000)}
- headers = {'x-csrf-token': repr(self._connection)}
-
- request = self._connection.get('notifications.json', headers=headers, params=params)
-
- if request.status_code != 200:
- raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
- return self._finalise(request.json())
-
- def _expand(self, new_notifications):
- ids = [notification.id for notification in self._notifications]
- notifications = self._notifications
- data = self._data
- for n in new_notifications:
- if n.id not in ids:
- if n.unread:
- data['unread_count'] +=1
- data['unread_count_by_type'][n.type] +=1
- notifications.append(n)
- ids.append(n.id)
- self._notifications = notifications
- self._data = data
-
- def _update(self, new_notifications):
- ids = [notification.id for notification in self._notifications]
- notifications = self._notifications
- data = self._data
-
- update = False
- if new_notifications[len(new_notifications)-1].id not in ids:
- update = True
-
- for i in range(len(new_notifications)):
- if new_notifications[-i].id not in ids:
- if new_notifications[-i].unread:
- data['unread_count'] +=1
- data['unread_count_by_type'][new_notifications[-i].type] +=1
- notifications = [new_notifications[-i]] + notifications
- ids.append(new_notifications[-i].id)
- self._notifications = notifications
- self._data = data
- if update: self.update() # if there is a gap
-
- def update(self, per_page=5, page=1):
- result = self.get(per_page=per_page, page=page)
- if result: self._update( result )
-
- def more(self, per_page=5, page=0):
- if not page: page = self.page + 1
- self.page = page
- result = self.get(per_page=per_page, page=page)
- if result:
- self._expand( result )
-
- def get(self, per_page=5, page=1):
- """Returns list of notifications.
- """
- params = {'per_page': per_page, 'page': page}
- headers = {'x-csrf-token': repr(self._connection)}
-
- request = self._connection.get('notifications.json', headers=headers, params=params)
-
- if request.status_code != 200:
- raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
- return self._finalise(request.json())
+ """This class represents notifications of a user.
+ """
+ def __init__(self, connection):
+ self._connection = connection
+ self._data = {}
+ self._notifications = self.get()
+ self.page = 1
+
+ def __iter__(self):
+ return iter(self._notifications)
+
+ def __getitem__(self, n):
+ return self._notifications[n]
+
+ def _finalise(self, notifications):
+ self._data['unread_count'] = notifications['unread_count']
+ self._data['unread_count_by_type'] = notifications['unread_count_by_type']
+ return [Notification(self._connection, n) for n in notifications.get('notification_list', [])]
+
+ def last(self):
+ """Returns list of most recent notifications.
+ """
+ params = {'per_page': 5, '_': int(round(time.time(), 3)*1000)}
+ headers = {'x-csrf-token': repr(self._connection)}
+
+ request = self._connection.get('notifications.json', headers=headers, params=params)
+
+ if request.status_code != 200:
+ raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
+ return self._finalise(request.json())
+
+ def _expand(self, new_notifications):
+ ids = [notification.id for notification in self._notifications]
+ notifications = self._notifications
+ data = self._data
+ for n in new_notifications:
+ if n.id not in ids:
+ if n.unread:
+ data['unread_count'] +=1
+ data['unread_count_by_type'][n.type] +=1
+ notifications.append(n)
+ ids.append(n.id)
+ self._notifications = notifications
+ self._data = data
+
+ def _update(self, new_notifications):
+ ids = [notification.id for notification in self._notifications]
+ notifications = self._notifications
+ data = self._data
+
+ update = False
+ if new_notifications[len(new_notifications)-1].id not in ids:
+ update = True
+
+ for i in range(len(new_notifications)):
+ if new_notifications[-i].id not in ids:
+ if new_notifications[-i].unread:
+ data['unread_count'] +=1
+ data['unread_count_by_type'][new_notifications[-i].type] +=1
+ notifications = [new_notifications[-i]] + notifications
+ ids.append(new_notifications[-i].id)
+ self._notifications = notifications
+ self._data = data
+ if update: self.update() # if there is a gap
+
+ def update(self, per_page=5, page=1):
+ result = self.get(per_page=per_page, page=page)
+ if result: self._update( result )
+
+ def more(self, per_page=5, page=0):
+ if not page: page = self.page + 1
+ self.page = page
+ result = self.get(per_page=per_page, page=page)
+ if result:
+ self._expand( result )
+
+ def get(self, per_page=5, page=1):
+ """Returns list of notifications.
+ """
+ params = {'per_page': per_page, 'page': page}
+ headers = {'x-csrf-token': repr(self._connection)}
+
+ request = self._connection.get('notifications.json', headers=headers, params=params)
+
+ if request.status_code != 200:
+ raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
+ return self._finalise(request.json())
def sephandle(handle):
- """Separate Diaspora* handle into pod pod and user.
+ """Separate Diaspora* handle into pod pod and user.
- :returns: two-tuple (pod, user)
- """
- if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
- raise errors.InvalidHandleError('{0}'.format(handle))
- handle = handle.split('@')
- pod, user = handle[1], handle[0]
- return (pod, user)
+ :returns: two-tuple (pod, user)
+ """
+ if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
+ raise errors.InvalidHandleError('{0}'.format(handle))
+ handle = handle.split('@')
+ pod, user = handle[1], handle[0]
+ return (pod, user)
class User():
- """This class abstracts a D* user.
- This object goes around the limitations of current D* API and will
- extract user data using black magic.
- However, no chickens are harmed when you use it.
-
- The parameter fetch should be either 'posts', 'data' or 'none'. By
- default it is 'posts' which means in addition to user data, stream
- will be fetched. If user has not posted yet diaspy will not be able
- to extract the information from his/her posts. Since there is no official
- way to do it we rely on user posts. If this will be the case user
- will be notified with appropriate exception message.
-
- If fetch is 'data', only user data will be fetched. If the user is
- not found, no exception will be returned.
-
- When creating new User() one can pass either guid, handle and/or id as
- optional parameters. GUID takes precedence over handle when fetching
- user stream. When fetching user data, handle is required.
- """
- @classmethod
- def parse(cls, connection, data):
- person = data.get('person')
- if person is None:
- raise errors.KeyMissingFromFetchedData('person', data)
-
- guid = person.get('guid')
- if guid is None:
- raise errors.KeyMissingFromFetchedData('guid', person)
-
- handle = person.get('diaspora_id')
- if handle is None:
- raise errors.KeyMissingFromFetchedData('diaspora_id', person)
-
- person_id = person.get('id')
- if person_id is None:
- raise errors.KeyMissingFromFetchedData('id', person)
-
- return User(connection, guid, handle, id, data=data)
-
- def __init__(self, connection, guid='', handle='', fetch='posts', id=0, data=None):
- self._connection = connection
- self.stream = []
- self.data = {
- 'guid': guid,
- 'handle': handle,
- 'id': id,
- }
- self.photos = []
- if data: self.data.update( data )
- if fetch: self._fetch(fetch)
-
- def __getitem__(self, key):
- return self.data[key]
-
- def __str__(self):
- return self.data.get('guid', '<guid missing>')
-
- def __repr__(self):
- return '{0} ({1})'.format(self.handle(), self.guid())
-
- def handle(self):
- if 'handle' in self.data: return self['handle']
- return self.data.get('diaspora_id', 'Unknown handle')
-
- def guid(self):
- return self.data.get('guid', '<guid missing>')
-
- def id(self):
- return self.data['id']
-
- def _fetchstream(self):
- self.stream = Outer(self._connection, guid=self['guid'])
-
- def _fetch(self, fetch):
- """Fetch user posts or data.
- """
- if fetch == 'posts':
- if self.handle() and not self['guid']: self.fetchhandle()
- else: self.fetchguid()
- elif fetch == 'data' and self['handle']:
- self.fetchprofile()
-
- def _finalize_data(self, data):
- """Adjustments are needed to have similar results returned
- by search feature and fetchguid()/fetchhandle().
- """
- return data
-
- def _postproc(self, request):
- """Makes necessary modifications to user data and
- sets up a stream.
-
- :param request: request object
- :type request: request
- """
- if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
- data = request.json()
- self.data = self._finalize_data(data)
-
- def fetchhandle(self, protocol='https'):
- """Fetch user data and posts using Diaspora handle.
- """
- pod, user = sephandle(self['handle'])
- request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
- self._postproc(request)
- self._fetchstream()
-
- def fetchguid(self, fetch_stream=True):
- """Fetch user data and posts (if fetch_stream is True) using guid.
- """
- if self['guid']:
- request = self._connection.get('people/{0}.json'.format(self['guid']))
- self._postproc(request)
- if fetch_stream: self._fetchstream()
- else:
- raise errors.UserError('GUID not set')
-
- def fetchprofile(self):
- """Fetches user data.
- """
- data = search.Search(self._connection).user(self.handle())
- if not data:
- raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self.handle(), self._connection.pod))
- else:
- self.data.update( data[0] )
-
- def aspectMemberships(self):
- if 'contact' in self.data:
- return self.data.get('contact', {}).get('aspect_memberships', [])
- else:
- return self.data.get('aspect_memberships', [])
-
- def getPhotos(self):
- """
- --> GET /people/{GUID}/photos.json HTTP/1.1
-
- <-- HTTP/1.1 200 OK
-
- {
- "photos":[
- {
- "id":{photo_id},
- "guid":"{photo_guid}",
- "created_at":"2018-03-08T23:48:31.000Z",
- "author":{
- "id":{author_id},
- "guid":"{author_guid}",
- "name":"{author_name}",
- "diaspora_id":"{diaspora_id}",
- "avatar":{"small":"{avatar_url_small}","medium":"{avatar_url_medium}","large":"{avatar_url_large}"}
- },
- "sizes":{
- "small":"{photo_url}",
- "medium":"{photo_url}",
- "large":"{photo_url}"
- },
- "dimensions":{"height":847,"width":998},
- "status_message":{
- "id":{post_id}
- }
- },{ ..
- }
-
- if there are no photo's it returns:
- {"photos":[]}
- """
-
- request = self._connection.get('/people/{0}/photos.json'.format(self['guid']))
- if request.status_code != 200: raise errors.UserError('could not fetch photos for user: {0}'.format(self['guid']))
-
- json = request.json()
- if json: self.photos = json['photos']
- return json['photos']
-
- def getHCard(self):
- """Returns json containing user HCard.
- --> /people/{guid}/hovercard.json?_={timestamp}
-
- <-- HTTP/2.0 200 OK
- {
- "id":123,
- "guid":"1234567890abcdef",
- "name":"test",
- "diaspora_id":"batman@test.test",
- "contact":false,
- "profile":{
- "avatar":"https://nicetesturl.url/image.jpg",
- "tags":["tag1", "tag2", "tag3", "tag4", "tag5"]}
- }
- """
- timestamp = int(time.mktime(time.gmtime()))
- request = self._connection.get('/people/{0}/hovercard.json?_={}'.format(self['guid'], timestamp))
- if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
- return request.json()
-
- def deletePhoto(self, photo_id):
- """
- --> DELETE /photos/{PHOTO_ID} HTTP/1.1
- <-- HTTP/1.1 204 No Content
- """
- request = self._connection.delete('/photos/{0}'.format(photo_id))
- if request.status_code != 204: raise errors.UserError('could not delete photo_id: {0}'.format(photo_id))
+ """This class abstracts a D* user.
+ This object goes around the limitations of current D* API and will
+ extract user data using black magic.
+ However, no chickens are harmed when you use it.
+
+ The parameter fetch should be either 'posts', 'data' or 'none'. By
+ default it is 'posts' which means in addition to user data, stream
+ will be fetched. If user has not posted yet diaspy will not be able
+ to extract the information from his/her posts. Since there is no official
+ way to do it we rely on user posts. If this will be the case user
+ will be notified with appropriate exception message.
+
+ If fetch is 'data', only user data will be fetched. If the user is
+ not found, no exception will be returned.
+
+ When creating new User() one can pass either guid, handle and/or id as
+ optional parameters. GUID takes precedence over handle when fetching
+ user stream. When fetching user data, handle is required.
+ """
+ @classmethod
+ def parse(cls, connection, data):
+ person = data.get('person')
+ if person is None:
+ raise errors.KeyMissingFromFetchedData('person', data)
+
+ guid = person.get('guid')
+ if guid is None:
+ raise errors.KeyMissingFromFetchedData('guid', person)
+
+ handle = person.get('diaspora_id')
+ if handle is None:
+ raise errors.KeyMissingFromFetchedData('diaspora_id', person)
+
+ person_id = person.get('id')
+ if person_id is None:
+ raise errors.KeyMissingFromFetchedData('id', person)
+
+ return User(connection, guid, handle, id, data=data)
+
+ def __init__(self, connection, guid='', handle='', fetch='posts', id=0, data=None):
+ self._connection = connection
+ self.stream = []
+ self.data = {
+ 'guid': guid,
+ 'handle': handle,
+ 'id': id,
+ }
+ self.photos = []
+ if data: self.data.update( data )
+ if fetch: self._fetch(fetch)
+
+ def __getitem__(self, key):
+ return self.data[key]
+
+ def __str__(self):
+ return self.data.get('guid', '<guid missing>')
+
+ def __repr__(self):
+ return '{0} ({1})'.format(self.handle(), self.guid())
+
+ def handle(self):
+ if 'handle' in self.data: return self['handle']
+ return self.data.get('diaspora_id', 'Unknown handle')
+
+ def guid(self):
+ return self.data.get('guid', '<guid missing>')
+
+ def id(self):
+ return self.data['id']
+
+ def _fetchstream(self):
+ self.stream = Outer(self._connection, guid=self['guid'])
+
+ def _fetch(self, fetch):
+ """Fetch user posts or data.
+ """
+ if fetch == 'posts':
+ if self.handle() and not self['guid']: self.fetchhandle()
+ else: self.fetchguid()
+ elif fetch == 'data' and self['handle']:
+ self.fetchprofile()
+
+ def _finalize_data(self, data):
+ """Adjustments are needed to have similar results returned
+ by search feature and fetchguid()/fetchhandle().
+ """
+ return data
+
+ def _postproc(self, request):
+ """Makes necessary modifications to user data and
+ sets up a stream.
+
+ :param request: request object
+ :type request: request
+ """
+ if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
+ data = request.json()
+ self.data = self._finalize_data(data)
+
+ def fetchhandle(self, protocol='https'):
+ """Fetch user data and posts using Diaspora handle.
+ """
+ pod, user = sephandle(self['handle'])
+ request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
+ self._postproc(request)
+ self._fetchstream()
+
+ def fetchguid(self, fetch_stream=True):
+ """Fetch user data and posts (if fetch_stream is True) using guid.
+ """
+ if self['guid']:
+ request = self._connection.get('people/{0}.json'.format(self['guid']))
+ self._postproc(request)
+ if fetch_stream: self._fetchstream()
+ else:
+ raise errors.UserError('GUID not set')
+
+ def fetchprofile(self):
+ """Fetches user data.
+ """
+ data = search.Search(self._connection).user(self.handle())
+ if not data:
+ raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self.handle(), self._connection.pod))
+ else:
+ self.data.update( data[0] )
+
+ def aspectMemberships(self):
+ if 'contact' in self.data:
+ return self.data.get('contact', {}).get('aspect_memberships', [])
+ else:
+ return self.data.get('aspect_memberships', [])
+
+ def getPhotos(self):
+ """
+ --> GET /people/{GUID}/photos.json HTTP/1.1
+
+ <-- HTTP/1.1 200 OK
+
+ {
+ "photos":[
+ {
+ "id":{photo_id},
+ "guid":"{photo_guid}",
+ "created_at":"2018-03-08T23:48:31.000Z",
+ "author":{
+ "id":{author_id},
+ "guid":"{author_guid}",
+ "name":"{author_name}",
+ "diaspora_id":"{diaspora_id}",
+ "avatar":{"small":"{avatar_url_small}","medium":"{avatar_url_medium}","large":"{avatar_url_large}"}
+ },
+ "sizes":{
+ "small":"{photo_url}",
+ "medium":"{photo_url}",
+ "large":"{photo_url}"
+ },
+ "dimensions":{"height":847,"width":998},
+ "status_message":{
+ "id":{post_id}
+ }
+ },{ ..
+ }
+
+ if there are no photo's it returns:
+ {"photos":[]}
+ """
+
+ request = self._connection.get('/people/{0}/photos.json'.format(self['guid']))
+ if request.status_code != 200: raise errors.UserError('could not fetch photos for user: {0}'.format(self['guid']))
+
+ json = request.json()
+ if json: self.photos = json['photos']
+ return json['photos']
+
+ def getHCard(self):
+ """Returns json containing user HCard.
+ --> /people/{guid}/hovercard.json?_={timestamp}
+
+ <-- HTTP/2.0 200 OK
+ {
+ "id":123,
+ "guid":"1234567890abcdef",
+ "name":"test",
+ "diaspora_id":"batman@test.test",
+ "contact":false,
+ "profile":{
+ "avatar":"https://nicetesturl.url/image.jpg",
+ "tags":["tag1", "tag2", "tag3", "tag4", "tag5"]}
+ }
+ """
+ timestamp = int(time.mktime(time.gmtime()))
+ request = self._connection.get('/people/{0}/hovercard.json?_={}'.format(self['guid'], timestamp))
+ if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
+ return request.json()
+
+ def deletePhoto(self, photo_id):
+ """
+ --> DELETE /photos/{PHOTO_ID} HTTP/1.1
+ <-- HTTP/1.1 204 No Content
+ """
+ request = self._connection.delete('/photos/{0}'.format(photo_id))
+ if request.status_code != 204: raise errors.UserError('could not delete photo_id: {0}'.format(photo_id))
class Me():
- """Object represetnting current user.
- """
- _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
- _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
+ """Object represetnting current user.
+ """
+ _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
+ _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
- def __init__(self, connection):
- self._connection = connection
+ def __init__(self, connection):
+ self._connection = connection
- def getInfo(self):
- """This function returns the current user's attributes.
+ def getInfo(self):
+ """This function returns the current user's attributes.
- :returns: dict
- """
- request = self._connection.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)
- return json.loads(userdata)
+ :returns: dict
+ """
+ request = self._connection.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)
+ return json.loads(userdata)
class Contacts():
- """This class represents user's list of contacts.
- """
- def __init__(self, connection, fetch=False, set=''):
- self._connection = connection
- self.contacts = None
- if fetch: self.contacts = self.get(set)
-
- def __getitem__(self, index):
- return self.contacts[index]
-
- def addAspect(self, name, visible=False):
- """
- --> POST /aspects HTTP/1.1
- --> {"person_id":null,"name":"test","contacts_visible":false}
-
- <-- HTTP/1.1 200 OK
-
- Add new aspect.
-
- TODO: status_code's
-
- :param name: aspect name to add
- :type name: str
- :param visible: sets if contacts in aspect are visible for each and other
- :type visible: bool
- :returns: JSON from request
- """
- data = {
- 'person_id': None,
- 'name': name,
- 'contacts_visible': visible
- }
- headers={'content-type': 'application/json',
- 'accept': 'application/json' }
- request = self._connection.tokenFrom('contacts').post('aspects', headers=headers, data=json.dumps(data))
-
- if request.status_code == 400:
- raise errors.AspectError('duplicate record, aspect alreadt exists: {0}'.format(request.status_code))
- elif request.status_code != 200:
- raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
- new_aspect = request.json()
- self._connection.userdata()['aspects'].append( new_aspect )
-
- return new_aspect
-
- def deleteAspect(self, aspect_id):
- """
- --> POST /aspects/{ASPECT_ID} HTTP/1.1
- _method=delete&authenticity_token={TOKEN}
- Content-Type: application/x-www-form-urlencoded
-
- <-- HTTP/1.1 302 Found
- Content-Type: text/html; charset=utf-8
- """
- request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format( aspect_id ))
-
- if request.status_code != 200: # since we don't post but delete
- raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
- def add(self, user_id, aspect_ids):
- """Add user to aspects of given ids.
-
- :param user_id: user id (not guid)
- :type user_id: str
- :param aspect_ids: list of aspect ids
- :type aspect_ids: list
- """
- # TODO update self.contacts
- # Returns {"aspect_id":123,"person_id":123}
- for aid in aspect_ids:
- new_aspect_membership = Aspect(self._connection, aid).addUser(user_id)
-
- # user.
- if new_aspect_membership:
- for user in self.contacts:
- if int(user.data['person_id']) == int(user_id):
- user.data['aspect_memberships'].append( new_aspect_membership )
- return new_aspect_membership
-
- def remove(self, user_id, aspect_ids):
- """Remove user from aspects of given ids.
-
- :param user_id: user id
- :type user_id: str
- :param aspect_ids: list of aspect ids
- :type aspect_ids: list
- """
- for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
-
- def get(self, set='', page=0):
- """Returns list of user contacts.
- Contact is a User() who is in one or more of user's
- aspects.
-
- By default, it will return list of users who are in
- user's aspects.
-
- If `set` is `all` it will also include users who only share
- with logged user and are not in his/hers aspects.
-
- If `set` is `only_sharing` it will return users who are only
- sharing with logged user and ARE NOT in his/hers aspects.
-
- # On "All contacts" button diaspora
- on the time of testing this I had 20 contacts and 10 that
- where only sharing with me. So 30 in total.
-
- --> GET /contacts?set=all HTTP/1.1
- <-- HTTP/1.1 200 OK
- returned 25 contacts (5 only sharing with me)
-
- --> GET /contacts.json?page=1&set=all&_=1524410225376 HTTP/1.1
- <-- HTTP/1.1 200 OK
- returned the same list as before.
-
- --> GET /contacts.json?page=2&set=all&_=1524410225377 HTTP/1.1
- <-- HTTP/1.1 200 OK
- returned the other 5 that where only sharing with me.
-
- --> GET /contacts.json?page=3&set=all&_=1524410225378 HTTP/1.1
- <-- HTTP/1.1 200 OK
- returned empty list.
-
- It appears that /contacts?set=all returns a maximum of 25
- contacts.
-
- So if /contacts?set=all returns 25 contacts then request next
- page until page returns a list with less then 25. I don't see a
- reason why we should request page=1 'cause the previous request
- will be the same. So begin with page=2 if /contacts?set=all
- returns 25.
-
- :param set: if passed could be 'all' or 'only_sharing'
- :type set: str
- """
- params = {}
- if set:
- params['set'] = set
- params['_'] = int(time.mktime(time.gmtime()))
- if page: params['page'] = page
-
- request = self._connection.get('contacts.json', params=params)
- if request.status_code != 200:
- raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
-
- json = request.json()
- users = [User.parse(self._connection, each) for each in json]
- if len(json) == 25:
- if not page: page = 1
- users += self.get(set=set, page=page+1)
- return users
+ """This class represents user's list of contacts.
+ """
+ def __init__(self, connection, fetch=False, set=''):
+ self._connection = connection
+ self.contacts = None
+ if fetch: self.contacts = self.get(set)
+
+ def __getitem__(self, index):
+ return self.contacts[index]
+
+ def addAspect(self, name, visible=False):
+ """
+ --> POST /aspects HTTP/1.1
+ --> {"person_id":null,"name":"test","contacts_visible":false}
+
+ <-- HTTP/1.1 200 OK
+
+ Add new aspect.
+
+ TODO: status_code's
+
+ :param name: aspect name to add
+ :type name: str
+ :param visible: sets if contacts in aspect are visible for each and other
+ :type visible: bool
+ :returns: JSON from request
+ """
+ data = {
+ 'person_id': None,
+ 'name': name,
+ 'contacts_visible': visible
+ }
+ headers={'content-type': 'application/json',
+ 'accept': 'application/json' }
+ request = self._connection.tokenFrom('contacts').post('aspects', headers=headers, data=json.dumps(data))
+
+ if request.status_code == 400:
+ raise errors.AspectError('duplicate record, aspect alreadt exists: {0}'.format(request.status_code))
+ elif request.status_code != 200:
+ raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+ new_aspect = request.json()
+ self._connection.userdata()['aspects'].append( new_aspect )
+
+ return new_aspect
+
+ def deleteAspect(self, aspect_id):
+ """
+ --> POST /aspects/{ASPECT_ID} HTTP/1.1
+ _method=delete&authenticity_token={TOKEN}
+ Content-Type: application/x-www-form-urlencoded
+
+ <-- HTTP/1.1 302 Found
+ Content-Type: text/html; charset=utf-8
+ """
+ request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format( aspect_id ))
+
+ if request.status_code != 200: # since we don't post but delete
+ raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+ def add(self, user_id, aspect_ids):
+ """Add user to aspects of given ids.
+
+ :param user_id: user id (not guid)
+ :type user_id: str
+ :param aspect_ids: list of aspect ids
+ :type aspect_ids: list
+ """
+ # TODO update self.contacts
+ # Returns {"aspect_id":123,"person_id":123}
+ for aid in aspect_ids:
+ new_aspect_membership = Aspect(self._connection, aid).addUser(user_id)
+
+ # user.
+ if new_aspect_membership:
+ for user in self.contacts:
+ if int(user.data['person_id']) == int(user_id):
+ user.data['aspect_memberships'].append( new_aspect_membership )
+ return new_aspect_membership
+
+ def remove(self, user_id, aspect_ids):
+ """Remove user from aspects of given ids.
+
+ :param user_id: user id
+ :type user_id: str
+ :param aspect_ids: list of aspect ids
+ :type aspect_ids: list
+ """
+ for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
+
+ def get(self, set='', page=0):
+ """Returns list of user contacts.
+ Contact is a User() who is in one or more of user's
+ aspects.
+
+ By default, it will return list of users who are in
+ user's aspects.
+
+ If `set` is `all` it will also include users who only share
+ with logged user and are not in his/hers aspects.
+
+ If `set` is `only_sharing` it will return users who are only
+ sharing with logged user and ARE NOT in his/hers aspects.
+
+ # On "All contacts" button diaspora
+ on the time of testing this I had 20 contacts and 10 that
+ where only sharing with me. So 30 in total.
+
+ --> GET /contacts?set=all HTTP/1.1
+ <-- HTTP/1.1 200 OK
+ returned 25 contacts (5 only sharing with me)
+
+ --> GET /contacts.json?page=1&set=all&_=1524410225376 HTTP/1.1
+ <-- HTTP/1.1 200 OK
+ returned the same list as before.
+
+ --> GET /contacts.json?page=2&set=all&_=1524410225377 HTTP/1.1
+ <-- HTTP/1.1 200 OK
+ returned the other 5 that where only sharing with me.
+
+ --> GET /contacts.json?page=3&set=all&_=1524410225378 HTTP/1.1
+ <-- HTTP/1.1 200 OK
+ returned empty list.
+
+ It appears that /contacts?set=all returns a maximum of 25
+ contacts.
+
+ So if /contacts?set=all returns 25 contacts then request next
+ page until page returns a list with less then 25. I don't see a
+ reason why we should request page=1 'cause the previous request
+ will be the same. So begin with page=2 if /contacts?set=all
+ returns 25.
+
+ :param set: if passed could be 'all' or 'only_sharing'
+ :type set: str
+ """
+ params = {}
+ if set:
+ params['set'] = set
+ params['_'] = int(time.mktime(time.gmtime()))
+ if page: params['page'] = page
+
+ request = self._connection.get('contacts.json', params=params)
+ if request.status_code != 200:
+ raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
+
+ json = request.json()
+ users = [User.parse(self._connection, each) for each in json]
+ if len(json) == 25:
+ if not page: page = 1
+ users += self.get(set=set, page=page+1)
+ return users
class Search():
- """This object is used for searching for content on Diaspora*.
- """
- def __init__(self, connection):
- self._connection = connection
-
- def lookupUser(self, handle):
- """This function will launch a webfinger lookup from the pod for the
- handle requested. Response code is returned and if the lookup was successful,
- user should soon be searchable via pod used for connection.
-
- :param string: Handle to search for.
- """
- request = self._connection.get('people', headers={'accept': 'text/html'}, params={'q': handle})
- return request.status_code
-
- def user(self, query):
- """Searches for a user.
- Will return list of dictionaries containing
- data of found users.
- """
- request = self._connection.get('people.json', params={'q': query, 'utf-8': '%u2713'})
- if request.status_code != 200:
- raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
- return request.json()
-
- def tags(self, query, limit=10):
- """Retrieve tag suggestions.
-
- :param query: query used to search
- :type query: str
- :param limit: maxmal number of suggestions returned
- :type limit: int
- """
- params = {'q': query, 'limit': limit}
- request = self._connection.get('tags', params=params, headers={'x-csrf-token': repr(self._connection)})
- if request.status_code != 200:
- raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
- return [i['name'] for i in request.json()]
+ """This object is used for searching for content on Diaspora*.
+ """
+ def __init__(self, connection):
+ self._connection = connection
+
+ def lookupUser(self, handle):
+ """This function will launch a webfinger lookup from the pod for the
+ handle requested. Response code is returned and if the lookup was successful,
+ user should soon be searchable via pod used for connection.
+
+ :param string: Handle to search for.
+ """
+ request = self._connection.get('people', headers={'accept': 'text/html'}, params={'q': handle})
+ return request.status_code
+
+ def user(self, query):
+ """Searches for a user.
+ Will return list of dictionaries containing
+ data of found users.
+ """
+ request = self._connection.get('people.json', params={'q': query, 'utf-8': '%u2713'})
+ if request.status_code != 200:
+ raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
+ return request.json()
+
+ def tags(self, query, limit=10):
+ """Retrieve tag suggestions.
+
+ :param query: query used to search
+ :type query: str
+ :param limit: maxmal number of suggestions returned
+ :type limit: int
+ """
+ params = {'q': query, 'limit': limit}
+ request = self._connection.get('tags', params=params, headers={'x-csrf-token': repr(self._connection)})
+ if request.status_code != 200:
+ raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
+ return [i['name'] for i in request.json()]
class Account():
- """Provides interface to account settings.
- """
- email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"')
- language_option_regexp = re.compile('<option value="([_a-zA-Z-]+)"(?: selected="selected")?>(.*?)</option>')
-
- def __init__(self, connection):
- self._connection = connection
-
- def downloadxml(self):
- """Returns downloaded XML.
- """
- request = self._connection.get('user/export')
- return request.text
-
- def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
- """Downloads photos into the current working directory.
- Sizes are: large, medium, small.
- Filename is: {post_guid}_{photo_guid}.{extension}
-
- Normally, this method will catch urllib-generated errors and
- just issue warnings about photos that couldn't be downloaded.
- However, with _critical param set to True errors will become
- critical - the will be reraised in finally block.
-
- :param size: size of the photos to download - large, medium or small
- :type size: str
- :param path: path to download (defaults to current working directory
- :type path: str
- :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
- :type mark_nsfw: bool
- :param _stream: diaspy.streams.Generic-like object (only for testing)
- :param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
-
- :returns: integer, number of photos downloaded
- """
- photos = 0
- if _stream is None:
- stream = streams.Activity(self._connection)
- stream.full()
- else:
- stream = _stream
- for i, post in enumerate(stream):
- if post['nsfw'] is not False: nsfw = '-nsfw'
- else: nsfw = ''
- if post['photos']:
- for n, photo in enumerate(post['photos']):
- # photo format -- .jpg, .png etc.
- ext = photo['sizes'][size].split('.')[-1]
- name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
- filename = os.path.join(path, name)
- try:
- urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
- except (urllib.error.HTTPError, urllib.error.URLError) as e:
- warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
- finally:
- if _critical: raise
- photos += 1
- return photos
-
- def setEmail(self, email):
- """Changes user's email.
- """
- data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
- request = self._connection.post('user', data=data, allow_redirects=False)
- if request.status_code != 302:
- raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
-
- def getEmail(self):
- """Returns currently used email.
- """
- data = self._connection.get('user/edit')
- email = self.email_regexp.search(data.text)
- if email is None: email = ''
- else: email = email.group(1)
- return email
-
- def setLanguage(self, lang):
- """Changes user's email.
-
- :param lang: language identifier from getLanguages()
- """
- data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
- request = self._connection.post('user', data=data, allow_redirects=False)
- if request.status_code != 302:
- raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
-
- def getLanguages(self):
- """Returns a list of tuples containing ('Language name', 'identifier').
- One of the Black Magic(tm) methods.
- """
- request = self._connection.get('user/edit')
- return self.language_option_regexp.findall(request.text)
+ """Provides interface to account settings.
+ """
+ email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"')
+ language_option_regexp = re.compile('<option value="([_a-zA-Z-]+)"(?: selected="selected")?>(.*?)</option>')
+
+ def __init__(self, connection):
+ self._connection = connection
+
+ def downloadxml(self):
+ """Returns downloaded XML.
+ """
+ request = self._connection.get('user/export')
+ return request.text
+
+ def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
+ """Downloads photos into the current working directory.
+ Sizes are: large, medium, small.
+ Filename is: {post_guid}_{photo_guid}.{extension}
+
+ Normally, this method will catch urllib-generated errors and
+ just issue warnings about photos that couldn't be downloaded.
+ However, with _critical param set to True errors will become
+ critical - the will be reraised in finally block.
+
+ :param size: size of the photos to download - large, medium or small
+ :type size: str
+ :param path: path to download (defaults to current working directory
+ :type path: str
+ :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
+ :type mark_nsfw: bool
+ :param _stream: diaspy.streams.Generic-like object (only for testing)
+ :param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
+
+ :returns: integer, number of photos downloaded
+ """
+ photos = 0
+ if _stream is None:
+ stream = streams.Activity(self._connection)
+ stream.full()
+ else:
+ stream = _stream
+ for i, post in enumerate(stream):
+ if post['nsfw'] is not False: nsfw = '-nsfw'
+ else: nsfw = ''
+ if post['photos']:
+ for n, photo in enumerate(post['photos']):
+ # photo format -- .jpg, .png etc.
+ ext = photo['sizes'][size].split('.')[-1]
+ name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
+ filename = os.path.join(path, name)
+ try:
+ urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
+ except (urllib.error.HTTPError, urllib.error.URLError) as e:
+ warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
+ finally:
+ if _critical: raise
+ photos += 1
+ return photos
+
+ def setEmail(self, email):
+ """Changes user's email.
+ """
+ data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
+ request = self._connection.post('user', data=data, allow_redirects=False)
+ if request.status_code != 302:
+ raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
+
+ def getEmail(self):
+ """Returns currently used email.
+ """
+ data = self._connection.get('user/edit')
+ email = self.email_regexp.search(data.text)
+ if email is None: email = ''
+ else: email = email.group(1)
+ return email
+
+ def setLanguage(self, lang):
+ """Changes user's email.
+
+ :param lang: language identifier from getLanguages()
+ """
+ data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
+ request = self._connection.post('user', data=data, allow_redirects=False)
+ if request.status_code != 302:
+ raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
+
+ def getLanguages(self):
+ """Returns a list of tuples containing ('Language name', 'identifier').
+ One of the Black Magic(tm) methods.
+ """
+ request = self._connection.get('user/edit')
+ return self.language_option_regexp.findall(request.text)
class Privacy():
- """Provides interface to provacy settings.
- """
- def __init__(self, connection):
- self._connection = connection
+ """Provides interface to provacy settings.
+ """
+ def __init__(self, connection):
+ self._connection = connection
class Profile():
- """Provides interface to profile settigns.
-
- WARNING:
-
- Because of the way update requests for profile are created every field must be sent.
- The `load()` method is used to load all information into the dictionary.
- Setters can then be used to adjust the data.
- Finally, `update()` can be called to send data back to pod.
- """
- firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
- lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
- bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>')
- location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
- gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
- birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>')
- birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>')
- birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>')
- is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
- is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
-
- def __init__(self, connection, no_load=False):
- self._connection = connection
- self.data = {'utf-8': '✓',
- '_method': 'put',
- 'profile[first_name]': '',
- 'profile[last_name]': '',
- 'profile[tag_string]': '',
- 'tags': '',
- 'file': '',
- 'profile[bio]': '',
- 'profile[location]': '',
- 'profile[gender]': '',
- 'profile[date][year]': '',
- 'profile[date][month]': '',
- 'profile[date][day]': '',
- }
- self._html = self._fetchhtml()
- self._loaded = False
- if not no_load: self.load()
-
- def _fetchhtml(self):
- """Fetches html that will be used to extract data.
- """
- return self._connection.get('profile/edit').text
-
- def getName(self):
- """Returns two-tuple: (first, last) name.
- """
- first = self.firstname_regexp.search(self._html).group(1)
- last = self.lastname_regexp.search(self._html).group(1)
- return (first, last)
-
- def getTags(self):
- """Returns tags user had selected when describing him/her-self.
- """
- guid = self._connection.getUserData()['guid']
- html = self._connection.get('people/{0}'.format(guid)).text
- description_regexp = re.compile('<a href="/tags/(.*?)" class="tag">#.*?</a>')
- return [tag.lower() for tag in re.findall(description_regexp, html)]
-
- def getBio(self):
- """Returns user bio.
- """
- bio = self.bio_regexp.search(self._html).group(1)
- return bio
-
- def getLocation(self):
- """Returns location string.
- """
- location = self.location_regexp.search(self._html).group(1)
- return location
-
- def getGender(self):
- """Returns location string.
- """
- gender = self.gender_regexp.search(self._html).group(1)
- return gender
-
- def getBirthDate(self, named_month=False):
- """Returns three-tuple: (year, month, day).
-
- :param named_month: if True, return name of the month instead of integer
- :type named_month: bool
- """
- year = self.birth_year_regexp.search(self._html)
- if year is None: year = -1
- else: year = int(year.group(1))
- month = self.birth_month_regexp.search(self._html)
- if month is None:
- if named_month: month = ''
- else: month = -1
- else:
- if named_month:
- month = month.group(2)
- else:
- month = int(month.group(1))
- day = self.birth_day_regexp.search(self._html)
- if day is None: day = -1
- else: day = int(day.group(1))
- return (year, month, day)
-
- def isSearchable(self):
- """Returns True if profile is searchable.
- """
- searchable = self.is_searchable_regexp.search(self._html)
- # this is because value="true" in every case so we just
- # check if the field is "checked"
- if searchable is None: searchable = False # if it isn't - the regexp just won't match
- else: searchable = True
- return searchable
-
- def isNSFW(self):
- """Returns True if profile is marked as NSFW.
- """
- nsfw = self.is_nsfw_regexp.search(self._html)
- if nsfw is None: nsfw = False
- else: nsfw = True
- return nsfw
-
- def setName(self, first, last):
- """Set first and last name.
- """
- self.data['profile[first_name]'] = first
- self.data['profile[last_name]'] = last
-
- def setTags(self, tags):
- """Sets tags that describe the user.
- """
- self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
-
- def setBio(self, bio):
- """Set bio of a user.
- """
- self.data['profile[bio]'] = bio
-
- def setLocation(self, location):
- """Set location of a user.
- """
- self.data['profile[location]'] = location
-
- def setGender(self, gender):
- """Set gender of a user.
- """
- self.data['profile[gender]'] = gender
-
- def setBirthDate(self, year, month, day):
- """Set birth date of a user.
- """
- self.data['profile[date][year]'] = year
- self.data['profile[date][month]'] = month
- self.data['profile[date][day]'] = day
-
- def setSearchable(self, searchable):
- """Set user's searchable status.
- """
- self.data['profile[searchable]'] = json.dumps(searchable)
-
- def setNSFW(self, nsfw):
- """Set user NSFW status.
- """
- self.data['profile[nsfw]'] = json.dumps(nsfw)
-
- def load(self):
- """Loads profile data into self.data dictionary.
- **Notice:** Not all keys are loaded yet.
- """
- self.setName(*self.getName())
- self.setBio(self.getBio())
- self.setLocation(self.getLocation())
- self.setGender(self.getGender())
- self.setBirthDate(*self.getBirthDate(named_month=False))
- self.setSearchable(self.isSearchable())
- self.setNSFW(self.isNSFW())
- self.setTags(self.getTags())
- self._loaded = True
-
- def update(self):
- """Updates profile information.
- """
- if not self._loaded: raise errors.DiaspyError('profile was not loaded')
- self.data['authenticity_token'] = repr(self._connection)
- print(self.data)
- request = self._connection.post('profile', data=self.data, allow_redirects=False)
- return request.status_code
+ """Provides interface to profile settigns.
+
+ WARNING:
+
+ Because of the way update requests for profile are created every field must be sent.
+ The `load()` method is used to load all information into the dictionary.
+ Setters can then be used to adjust the data.
+ Finally, `update()` can be called to send data back to pod.
+ """
+ firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
+ lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
+ bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>')
+ location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
+ gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
+ birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>')
+ birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>')
+ birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>')
+ is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
+ is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
+
+ def __init__(self, connection, no_load=False):
+ self._connection = connection
+ self.data = {'utf-8': '✓',
+ '_method': 'put',
+ 'profile[first_name]': '',
+ 'profile[last_name]': '',
+ 'profile[tag_string]': '',
+ 'tags': '',
+ 'file': '',
+ 'profile[bio]': '',
+ 'profile[location]': '',
+ 'profile[gender]': '',
+ 'profile[date][year]': '',
+ 'profile[date][month]': '',
+ 'profile[date][day]': '',
+ }
+ self._html = self._fetchhtml()
+ self._loaded = False
+ if not no_load: self.load()
+
+ def _fetchhtml(self):
+ """Fetches html that will be used to extract data.
+ """
+ return self._connection.get('profile/edit').text
+
+ def getName(self):
+ """Returns two-tuple: (first, last) name.
+ """
+ first = self.firstname_regexp.search(self._html).group(1)
+ last = self.lastname_regexp.search(self._html).group(1)
+ return (first, last)
+
+ def getTags(self):
+ """Returns tags user had selected when describing him/her-self.
+ """
+ guid = self._connection.getUserData()['guid']
+ html = self._connection.get('people/{0}'.format(guid)).text
+ description_regexp = re.compile('<a href="/tags/(.*?)" class="tag">#.*?</a>')
+ return [tag.lower() for tag in re.findall(description_regexp, html)]
+
+ def getBio(self):
+ """Returns user bio.
+ """
+ bio = self.bio_regexp.search(self._html).group(1)
+ return bio
+
+ def getLocation(self):
+ """Returns location string.
+ """
+ location = self.location_regexp.search(self._html).group(1)
+ return location
+
+ def getGender(self):
+ """Returns location string.
+ """
+ gender = self.gender_regexp.search(self._html).group(1)
+ return gender
+
+ def getBirthDate(self, named_month=False):
+ """Returns three-tuple: (year, month, day).
+
+ :param named_month: if True, return name of the month instead of integer
+ :type named_month: bool
+ """
+ year = self.birth_year_regexp.search(self._html)
+ if year is None: year = -1
+ else: year = int(year.group(1))
+ month = self.birth_month_regexp.search(self._html)
+ if month is None:
+ if named_month: month = ''
+ else: month = -1
+ else:
+ if named_month:
+ month = month.group(2)
+ else:
+ month = int(month.group(1))
+ day = self.birth_day_regexp.search(self._html)
+ if day is None: day = -1
+ else: day = int(day.group(1))
+ return (year, month, day)
+
+ def isSearchable(self):
+ """Returns True if profile is searchable.
+ """
+ searchable = self.is_searchable_regexp.search(self._html)
+ # this is because value="true" in every case so we just
+ # check if the field is "checked"
+ if searchable is None: searchable = False # if it isn't - the regexp just won't match
+ else: searchable = True
+ return searchable
+
+ def isNSFW(self):
+ """Returns True if profile is marked as NSFW.
+ """
+ nsfw = self.is_nsfw_regexp.search(self._html)
+ if nsfw is None: nsfw = False
+ else: nsfw = True
+ return nsfw
+
+ def setName(self, first, last):
+ """Set first and last name.
+ """
+ self.data['profile[first_name]'] = first
+ self.data['profile[last_name]'] = last
+
+ def setTags(self, tags):
+ """Sets tags that describe the user.
+ """
+ self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
+
+ def setBio(self, bio):
+ """Set bio of a user.
+ """
+ self.data['profile[bio]'] = bio
+
+ def setLocation(self, location):
+ """Set location of a user.
+ """
+ self.data['profile[location]'] = location
+
+ def setGender(self, gender):
+ """Set gender of a user.
+ """
+ self.data['profile[gender]'] = gender
+
+ def setBirthDate(self, year, month, day):
+ """Set birth date of a user.
+ """
+ self.data['profile[date][year]'] = year
+ self.data['profile[date][month]'] = month
+ self.data['profile[date][day]'] = day
+
+ def setSearchable(self, searchable):
+ """Set user's searchable status.
+ """
+ self.data['profile[searchable]'] = json.dumps(searchable)
+
+ def setNSFW(self, nsfw):
+ """Set user NSFW status.
+ """
+ self.data['profile[nsfw]'] = json.dumps(nsfw)
+
+ def load(self):
+ """Loads profile data into self.data dictionary.
+ **Notice:** Not all keys are loaded yet.
+ """
+ self.setName(*self.getName())
+ self.setBio(self.getBio())
+ self.setLocation(self.getLocation())
+ self.setGender(self.getGender())
+ self.setBirthDate(*self.getBirthDate(named_month=False))
+ self.setSearchable(self.isSearchable())
+ self.setNSFW(self.isNSFW())
+ self.setTags(self.getTags())
+ self._loaded = True
+
+ def update(self):
+ """Updates profile information.
+ """
+ if not self._loaded: raise errors.DiaspyError('profile was not loaded')
+ self.data['authenticity_token'] = repr(self._connection)
+ print(self.data)
+ request = self._connection.post('profile', data=self.data, allow_redirects=False)
+ return request.status_code
class Services():
- """Provides interface to services settings.
- """
- def __init__(self, connection):
- self._connection = connection
+ """Provides interface to services settings.
+ """
+ def __init__(self, connection):
+ self._connection = connection
stream, so we can use it for the more() function.
"""
try:
- import dateutil.parser
- def parse_utc_timestamp(date_str):
- return round(dateutil.parser.parse(date_str).timestamp())
+ import dateutil.parser
+ def parse_utc_timestamp(date_str):
+ return round(dateutil.parser.parse(date_str).timestamp())
except ImportError:
- try:
- from datetime import datetime
- from pytz import timezone
- def parse_utc_timestamp(date_str):
- return round(datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone('UTC')).timestamp())
+ try:
+ from datetime import datetime
+ from pytz import timezone
+ def parse_utc_timestamp(date_str):
+ return round(datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone('UTC')).timestamp())
- except ImportError:
- print("Please install either python-dateutil or python-pytz")
- exit # TODO raise exception
+ except ImportError:
+ print("Please install either python-dateutil or python-pytz")
+ exit # TODO raise exception
class Generic():
- """Object representing generic stream.
- """
- _location = 'stream.json'
-
- def __init__(self, connection, location='', fetch=True):
- """
- :param connection: Connection() object
- :type connection: diaspy.connection.Connection
- :param location: location of json (optional)
- :type location: str
- :param fetch: will call .fill() if true
- :type fetch: bool
- """
- self._connection = connection
- if location: self._location = location
- self.latest = None
- self._stream = []
- # since epoch
- self.max_time = int(time.mktime(time.gmtime()))
- if fetch: self.fill()
-
- def __contains__(self, post):
- """Returns True if stream contains given post.
- """
- return post in self._stream
-
- def __iter__(self):
- """Provides iterable interface for stream.
- """
- return iter(self._stream)
-
- def __getitem__(self, n):
- """Returns n-th item in Stream.
- """
- return self._stream[n]
-
- def __len__(self):
- """Returns length of the Stream.
- """
- return len(self._stream)
-
- def _obtain(self, max_time=0, suppress=True):
- """Obtains stream from pod.
-
- suppress:bool - suppress post-fetching errors (e.g. 404)
- """
- params = {}
- if max_time:
- if self.latest == None:
- self.latest = int(time.mktime(time.gmtime()) * 1000)
- self.latest -= max_time
- else: self.latest += 1
- params['max_time'] = max_time
- params['_'] = self.latest
- print("Diaspy _obtain.params: {}".format(params))
- request = self._connection.get(self._location, params=params)
- if request.status_code != 200:
- raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
- posts = []
- latest_time = None # Used to get the created_at from the latest posts we received.
- for post in request.json():
- try:
- comments = False
- if post['interactions']['comments_count'] > 3: comments = True
- posts.append(Post(self._connection, id=post['id'], guid=post['guid'], fetch=False, comments=comments, post_data=post))
- if post['created_at']: latest_time = post['created_at']
- except errors.PostError:
- if not suppress:
- raise
- if latest_time:
- self.max_time = parse_utc_timestamp( latest_time )
- return posts
-
- def _expand(self, new_stream):
- """Appends older posts to stream.
- """
- guids = [post.guid for post in self._stream]
- stream = self._stream
- for post in new_stream:
- if post.guid not in guids:
- stream.append(post)
- guids.append(post.guid)
- self._stream = stream
-
- def _update(self, new_stream):
- """Updates stream with new posts.
- """
- guids = [post.guid for post in self._stream]
-
- stream = self._stream
- for i in range(len(new_stream)):
- if new_stream[-i].guid not in guids:
- stream = [new_stream[-i]] + stream
- guids.append(new_stream[-i].guid)
- self._stream = stream
-
- def clear(self):
- """Set stream to empty.
- """
- self._stream = []
-
- def purge(self):
- """Removes all unexistent posts from stream.
- """
- stream = []
- for post in self._stream:
- deleted = False
- try:
- # error will tell us that the post has been deleted
- post.update()
- except Exception:
- deleted = True
- finally:
- if not deleted: stream.append(post)
- self._stream = stream
-
- def update(self):
- """Updates stream with new posts.
- """
- self._update(self._obtain())
-
- def fill(self):
- """Fills the stream with posts.
-
- **Notice:** this will create entirely new list of posts.
- If you want to preseve posts already present in stream use update().
- """
- self._stream = self._obtain()
-
- def more(self, max_time=0, backtime=86400):
- """Tries to download more (older posts) posts from Stream.
-
- TODO backtime isn't used anymore.
- Diaspora reference: https://github.com/diaspora/diaspora/blob/26a9e50ef935628c800f9a21d345057556fa5c31/app/helpers/stream_helper.rb#L48
-
- :param backtime: how many seconds substract each time (defaults to one day)
- :type backtime: int
- :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
- :type max_time: int
- """
-
- if not max_time: max_time = self.max_time
- self.max_time = max_time
- new_stream = self._obtain(max_time=max_time)
- self._expand(new_stream)
-
- def full(self, backtime=86400, retry=42, callback=None):
- """Fetches full stream - containing all posts.
- WARNING: this is a **VERY** long running function.
- Use callback parameter to access information about the stream during its
- run.
-
- Default backtime is one day. But sometimes user might not have any activity for longer
- period (in the beginning of my D* activity I was posting once a month or so).
- The role of retry is to hadle such situations by trying to go further back in time.
- If a post is found the counter is restored.
-
- Default retry is 42. If you don't know why go to the nearest library (or to the nearest
- Piratebay mirror) and grab a copy of "A Hitchhiker's Guide to the Galaxy" and read the
- book to find out. This will also increase your level of geekiness and you'll have a
- great time reading the book.
-
- :param backtime: how many seconds to substract each time
- :type backtime: int
- :param retry: how many times the functin should look deeper than your last post
- :type retry: int
- :param callback: callable taking diaspy.streams.Generic as an argument
- :returns: integer, lenght of the stream
- """
- oldstream = self.copy()
- self.more()
- while len(oldstream) < len(self):
- oldstream = self.copy()
- if callback is not None: callback(self)
- self.more(backtime=backtime)
- if len(oldstream) < len(self): continue
- # but if no posts were found start retrying...
- print('retrying... {0}'.format(retry))
- n = retry
- while n > 0:
- print('\t', n, self.max_time)
- # try to get even more posts...
- self.more(backtime=backtime)
- print('\t', len(oldstream), len(self))
- # check if it was a success...
- if len(oldstream) < len(self):
- # and if so restore normal order of execution by
- # going one loop higher
- break
- oldstream = self.copy()
- # if it was not a success substract one backtime, keep calm and
- # try going further back in time...
- n -= 1
- # check the comment below
- # no commented code should be present in good software
- #if len(oldstream) == len(self): break
- return len(self)
-
- def copy(self):
- """Returns copy (list of posts) of current stream.
- """
- return [p for p in self._stream]
-
- def json(self, comments=False, **kwargs):
- """Returns JSON encoded string containing stream's data.
-
- :param comments: to include comments or not to include 'em, that is the question this param holds answer to
- :type comments: bool
- """
- stream = [post for post in self._stream]
- if comments:
- for i, post in enumerate(stream):
- post._fetchcomments()
- comments = [c.data for c in post.comments]
- post['interactions']['comments'] = comments
- stream[i] = post
- stream = [post._data for post in stream]
- return json.dumps(stream, **kwargs)
+ """Object representing generic stream.
+ """
+ _location = 'stream.json'
+
+ def __init__(self, connection, location='', fetch=True):
+ """
+ :param connection: Connection() object
+ :type connection: diaspy.connection.Connection
+ :param location: location of json (optional)
+ :type location: str
+ :param fetch: will call .fill() if true
+ :type fetch: bool
+ """
+ self._connection = connection
+ if location: self._location = location
+ self.latest = None
+ self._stream = []
+ # since epoch
+ self.max_time = int(time.mktime(time.gmtime()))
+ if fetch: self.fill()
+
+ def __contains__(self, post):
+ """Returns True if stream contains given post.
+ """
+ return post in self._stream
+
+ def __iter__(self):
+ """Provides iterable interface for stream.
+ """
+ return iter(self._stream)
+
+ def __getitem__(self, n):
+ """Returns n-th item in Stream.
+ """
+ return self._stream[n]
+
+ def __len__(self):
+ """Returns length of the Stream.
+ """
+ return len(self._stream)
+
+ def _obtain(self, max_time=0, suppress=True):
+ """Obtains stream from pod.
+
+ suppress:bool - suppress post-fetching errors (e.g. 404)
+ """
+ params = {}
+ if max_time:
+ if self.latest == None:
+ self.latest = int(time.mktime(time.gmtime()) * 1000)
+ self.latest -= max_time
+ else: self.latest += 1
+ params['max_time'] = max_time
+ params['_'] = self.latest
+ print("Diaspy _obtain.params: {}".format(params))
+ request = self._connection.get(self._location, params=params)
+ if request.status_code != 200:
+ raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ posts = []
+ latest_time = None # Used to get the created_at from the latest posts we received.
+ for post in request.json():
+ try:
+ comments = False
+ if post['interactions']['comments_count'] > 3: comments = True
+ posts.append(Post(self._connection, id=post['id'], guid=post['guid'], fetch=False, comments=comments, post_data=post))
+ if post['created_at']: latest_time = post['created_at']
+ except errors.PostError:
+ if not suppress:
+ raise
+ if latest_time:
+ self.max_time = parse_utc_timestamp( latest_time )
+ return posts
+
+ def _expand(self, new_stream):
+ """Appends older posts to stream.
+ """
+ guids = [post.guid for post in self._stream]
+ stream = self._stream
+ for post in new_stream:
+ if post.guid not in guids:
+ stream.append(post)
+ guids.append(post.guid)
+ self._stream = stream
+
+ def _update(self, new_stream):
+ """Updates stream with new posts.
+ """
+ guids = [post.guid for post in self._stream]
+
+ stream = self._stream
+ for i in range(len(new_stream)):
+ if new_stream[-i].guid not in guids:
+ stream = [new_stream[-i]] + stream
+ guids.append(new_stream[-i].guid)
+ self._stream = stream
+
+ def clear(self):
+ """Set stream to empty.
+ """
+ self._stream = []
+
+ def purge(self):
+ """Removes all unexistent posts from stream.
+ """
+ stream = []
+ for post in self._stream:
+ deleted = False
+ try:
+ # error will tell us that the post has been deleted
+ post.update()
+ except Exception:
+ deleted = True
+ finally:
+ if not deleted: stream.append(post)
+ self._stream = stream
+
+ def update(self):
+ """Updates stream with new posts.
+ """
+ self._update(self._obtain())
+
+ def fill(self):
+ """Fills the stream with posts.
+
+ **Notice:** this will create entirely new list of posts.
+ If you want to preseve posts already present in stream use update().
+ """
+ self._stream = self._obtain()
+
+ def more(self, max_time=0, backtime=86400):
+ """Tries to download more (older posts) posts from Stream.
+
+ TODO backtime isn't used anymore.
+ Diaspora reference: https://github.com/diaspora/diaspora/blob/26a9e50ef935628c800f9a21d345057556fa5c31/app/helpers/stream_helper.rb#L48
+
+ :param backtime: how many seconds substract each time (defaults to one day)
+ :type backtime: int
+ :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
+ :type max_time: int
+ """
+
+ if not max_time: max_time = self.max_time
+ self.max_time = max_time
+ new_stream = self._obtain(max_time=max_time)
+ self._expand(new_stream)
+
+ def full(self, backtime=86400, retry=42, callback=None):
+ """Fetches full stream - containing all posts.
+ WARNING: this is a **VERY** long running function.
+ Use callback parameter to access information about the stream during its
+ run.
+
+ Default backtime is one day. But sometimes user might not have any activity for longer
+ period (in the beginning of my D* activity I was posting once a month or so).
+ The role of retry is to hadle such situations by trying to go further back in time.
+ If a post is found the counter is restored.
+
+ Default retry is 42. If you don't know why go to the nearest library (or to the nearest
+ Piratebay mirror) and grab a copy of "A Hitchhiker's Guide to the Galaxy" and read the
+ book to find out. This will also increase your level of geekiness and you'll have a
+ great time reading the book.
+
+ :param backtime: how many seconds to substract each time
+ :type backtime: int
+ :param retry: how many times the functin should look deeper than your last post
+ :type retry: int
+ :param callback: callable taking diaspy.streams.Generic as an argument
+ :returns: integer, lenght of the stream
+ """
+ oldstream = self.copy()
+ self.more()
+ while len(oldstream) < len(self):
+ oldstream = self.copy()
+ if callback is not None: callback(self)
+ self.more(backtime=backtime)
+ if len(oldstream) < len(self): continue
+ # but if no posts were found start retrying...
+ print('retrying... {0}'.format(retry))
+ n = retry
+ while n > 0:
+ print('\t', n, self.max_time)
+ # try to get even more posts...
+ self.more(backtime=backtime)
+ print('\t', len(oldstream), len(self))
+ # check if it was a success...
+ if len(oldstream) < len(self):
+ # and if so restore normal order of execution by
+ # going one loop higher
+ break
+ oldstream = self.copy()
+ # if it was not a success substract one backtime, keep calm and
+ # try going further back in time...
+ n -= 1
+ # check the comment below
+ # no commented code should be present in good software
+ #if len(oldstream) == len(self): break
+ return len(self)
+
+ def copy(self):
+ """Returns copy (list of posts) of current stream.
+ """
+ return [p for p in self._stream]
+
+ def json(self, comments=False, **kwargs):
+ """Returns JSON encoded string containing stream's data.
+
+ :param comments: to include comments or not to include 'em, that is the question this param holds answer to
+ :type comments: bool
+ """
+ stream = [post for post in self._stream]
+ if comments:
+ for i, post in enumerate(stream):
+ post._fetchcomments()
+ comments = [c.data for c in post.comments]
+ post['interactions']['comments'] = comments
+ stream[i] = post
+ stream = [post._data for post in stream]
+ return json.dumps(stream, **kwargs)
class Outer(Generic):
- """Object used by diaspy.models.User to represent
- stream of other user.
- """
- def __init__(self, connection, guid, fetch=True):
- location = 'people/{}/stream.json'.format(guid)
- super().__init__(connection, location, fetch)
+ """Object used by diaspy.models.User to represent
+ stream of other user.
+ """
+ def __init__(self, connection, guid, fetch=True):
+ location = 'people/{}/stream.json'.format(guid)
+ super().__init__(connection, location, fetch)
class Stream(Generic):
- """The main stream containing the combined posts of the
- followed users and tags and the community spotlights posts
- if the user enabled those.
- """
- location = 'stream.json'
-
- def post(self, text='', aspect_ids='public', photos=None, photo='', poll_question=None, poll_answers=None, location_coords=None, provider_display_name=''):
- """This function sends a post to an aspect.
- If both `photo` and `photos` are specified `photos` takes precedence.
-
- :param text: Text to post.
- :type text: str
-
- :param aspect_ids: Aspect ids to send post to.
- :type aspect_ids: str
-
- :param photo: filename of photo to post
- :type photo: str
-
- :param photos: id of photo to post (obtained from _photoupload())
- :type photos: int
-
- :param provider_display_name: name of provider displayed under the post
- :type provider_display_name: str
-
- :param poll_question: Question string
- :type poll_question: str
-
- :param poll_answers: Anwsers to the poll
- :type poll_answers: list with strings
-
- :param location_coords: TODO
- :type location_coords: TODO
-
- :returns: diaspy.models.Post -- the Post which has been created
- """
- data = {}
- data['aspect_ids'] = aspect_ids
- data['status_message'] = {'text': text, 'provider_display_name': provider_display_name}
- if photo: data['photos'] = self._photoupload(photo)
- if photos: data['photos'] = photos
- if poll_question and poll_answers:
- data['poll_question'] = poll_question
- data['poll_answers'] = poll_answers
- if location_coords: data['location_coords'] = location_coords
-
- request = self._connection.post('status_messages',
- data=json.dumps(data),
- headers={'content-type': 'application/json',
- 'accept': 'application/json',
- 'x-csrf-token': repr(self._connection)})
- if request.status_code != 201:
- raise Exception('{0}: Post could not be posted.'.format(request.status_code))
- post_json = request.json()
- post = Post(self._connection, id=post_json['id'], guid=post_json['guid'], post_data=post_json)
- return post
-
- def _photoupload(self, filename, aspects=[]):
- """Uploads picture to the pod.
-
- :param filename: path to picture file
- :type filename: str
- :param aspect_ids: list of ids of aspects to which you want to upload this photo
- :type aspect_ids: list of integers
-
- :returns: id of the photo being uploaded
- """
- data = open(filename, 'rb')
- image = data.read()
- data.close()
-
- params = {}
- params['photo[pending]'] = 'true'
- params['set_profile_image'] = ''
- params['qqfile'] = filename
- if not aspects: aspects = self._connection.getUserData()['aspects']
- for i, aspect in enumerate(aspects):
- params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
-
- headers = {'content-type': 'application/octet-stream',
- 'x-csrf-token': repr(self._connection),
- 'x-file-name': filename}
-
- request = self._connection.post('photos', data=image, params=params, headers=headers)
- if request.status_code != 200:
- raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
- return request.json()['data']['photo']['id']
+ """The main stream containing the combined posts of the
+ followed users and tags and the community spotlights posts
+ if the user enabled those.
+ """
+ location = 'stream.json'
+
+ def post(self, text='', aspect_ids='public', photos=None, photo='', poll_question=None, poll_answers=None, location_coords=None, provider_display_name=''):
+ """This function sends a post to an aspect.
+ If both `photo` and `photos` are specified `photos` takes precedence.
+
+ :param text: Text to post.
+ :type text: str
+
+ :param aspect_ids: Aspect ids to send post to.
+ :type aspect_ids: str
+
+ :param photo: filename of photo to post
+ :type photo: str
+
+ :param photos: id of photo to post (obtained from _photoupload())
+ :type photos: int
+
+ :param provider_display_name: name of provider displayed under the post
+ :type provider_display_name: str
+
+ :param poll_question: Question string
+ :type poll_question: str
+
+ :param poll_answers: Anwsers to the poll
+ :type poll_answers: list with strings
+
+ :param location_coords: TODO
+ :type location_coords: TODO
+
+ :returns: diaspy.models.Post -- the Post which has been created
+ """
+ data = {}
+ data['aspect_ids'] = aspect_ids
+ data['status_message'] = {'text': text, 'provider_display_name': provider_display_name}
+ if photo: data['photos'] = self._photoupload(photo)
+ if photos: data['photos'] = photos
+ if poll_question and poll_answers:
+ data['poll_question'] = poll_question
+ data['poll_answers'] = poll_answers
+ if location_coords: data['location_coords'] = location_coords
+
+ request = self._connection.post('status_messages',
+ data=json.dumps(data),
+ headers={'content-type': 'application/json',
+ 'accept': 'application/json',
+ 'x-csrf-token': repr(self._connection)})
+ if request.status_code != 201:
+ raise Exception('{0}: Post could not be posted.'.format(request.status_code))
+ post_json = request.json()
+ post = Post(self._connection, id=post_json['id'], guid=post_json['guid'], post_data=post_json)
+ return post
+
+ def _photoupload(self, filename, aspects=[]):
+ """Uploads picture to the pod.
+
+ :param filename: path to picture file
+ :type filename: str
+ :param aspect_ids: list of ids of aspects to which you want to upload this photo
+ :type aspect_ids: list of integers
+
+ :returns: id of the photo being uploaded
+ """
+ data = open(filename, 'rb')
+ image = data.read()
+ data.close()
+
+ params = {}
+ params['photo[pending]'] = 'true'
+ params['set_profile_image'] = ''
+ params['qqfile'] = filename
+ if not aspects: aspects = self._connection.getUserData()['aspects']
+ for i, aspect in enumerate(aspects):
+ params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
+
+ headers = {'content-type': 'application/octet-stream',
+ 'x-csrf-token': repr(self._connection),
+ 'x-file-name': filename}
+
+ request = self._connection.post('photos', data=image, params=params, headers=headers)
+ if request.status_code != 200:
+ raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
+ return request.json()['data']['photo']['id']
class Activity(Stream):
- """Stream representing user's activity.
- """
- _location = 'activity.json'
-
- def _delid(self, id):
- """Deletes post with given id.
- """
- post = None
- for p in self._stream:
- if p['id'] == id:
- post = p
- break
- if post is not None: post.delete()
-
- def delete(self, post):
- """Deletes post from users activity.
- `post` can be either post id or Post()
- object which will be identified and deleted.
- After deleting post the stream will be purged.
-
- :param post: post identifier
- :type post: str, diaspy.models.Post
- """
- if type(post) == str: self._delid(post)
- elif type(post) == Post: post.delete()
- else: raise TypeError('this method accepts str or Post types: {0} given')
- self.purge()
+ """Stream representing user's activity.
+ """
+ _location = 'activity.json'
+
+ def _delid(self, id):
+ """Deletes post with given id.
+ """
+ post = None
+ for p in self._stream:
+ if p['id'] == id:
+ post = p
+ break
+ if post is not None: post.delete()
+
+ def delete(self, post):
+ """Deletes post from users activity.
+ `post` can be either post id or Post()
+ object which will be identified and deleted.
+ After deleting post the stream will be purged.
+
+ :param post: post identifier
+ :type post: str, diaspy.models.Post
+ """
+ if type(post) == str: self._delid(post)
+ elif type(post) == Post: post.delete()
+ else: raise TypeError('this method accepts str or Post types: {0} given')
+ self.purge()
class Aspects(Generic):
- """This stream contains the posts filtered by
- the specified aspect IDs. You can choose the aspect IDs with
- the parameter `aspect_ids` which value should be
- a comma seperated list of aspect IDs.
- If the parameter is ommitted all aspects are assumed.
- An example call would be `aspects.json?aspect_ids=23,5,42`
- """
- _location = 'aspects.json'
-
- def getAspectID(self, aspect_name):
- """Returns id of an aspect of given name.
- Returns -1 if aspect is not found.
-
- :param aspect_name: aspect name (must be spelled exactly as when created)
- :type aspect_name: str
- :returns: int
- """
- id = -1
- aspects = self._connection.getUserData()['aspects']
- for aspect in aspects:
- if aspect['name'] == aspect_name: id = aspect['id']
- return id
-
- def filter(self, ids):
- """Filters posts by given aspect ids.
-
- :parameter ids: list of apsect ids
- :type ids: list of integers
- """
- self._location = 'aspects.json?a_ids[]=' + '{}'.format('&a_ids[]='.join(ids))
- self.fill() # this will create entirely new list of posts.
-
- def add(self, aspect_name, visible=0):
- """This function adds a new aspect.
- Status code 422 is accepted because it is returned by D* when
- you try to add aspect already present on your aspect list.
-
- :param aspect_name: name of aspect to create
- :param visible: whether the contacts in this aspect are visible to each other or not
-
- :returns: Aspect() object of just created aspect
- """
- data = {'authenticity_token': repr(self._connection),
- 'aspect[name]': aspect_name,
- 'aspect[contacts_visible]': visible}
-
- request = self._connection.post('aspects', data=data)
- if request.status_code not in [200, 422]:
- raise Exception('wrong status code: {0}'.format(request.status_code))
-
- id = self.getAspectID(aspect_name)
- return Aspect(self._connection, id)
-
- def remove(self, id=-1, name=''):
- """This method removes an aspect.
- You can give it either id or name of the aspect.
- When both are specified, id takes precedence over name.
-
- Status code 500 is accepted because although the D* will
- go nuts it will remove the aspect anyway.
-
- :param aspect_id: id fo aspect to remove
- :type aspect_id: int
- :param name: name of aspect to remove
- :type name: str
- """
- if id == -1 and name: id = self.getAspectID(name)
- data = {'_method': 'delete',
- 'authenticity_token': repr(self._connection)}
- request = self._connection.post('aspects/{0}'.format(id), data=data)
- if request.status_code not in [200, 302, 500]:
- raise Exception('wrong status code: {0}: cannot remove aspect'.format(request.status_code))
+ """This stream contains the posts filtered by
+ the specified aspect IDs. You can choose the aspect IDs with
+ the parameter `aspect_ids` which value should be
+ a comma seperated list of aspect IDs.
+ If the parameter is ommitted all aspects are assumed.
+ An example call would be `aspects.json?aspect_ids=23,5,42`
+ """
+ _location = 'aspects.json'
+
+ def getAspectID(self, aspect_name):
+ """Returns id of an aspect of given name.
+ Returns -1 if aspect is not found.
+
+ :param aspect_name: aspect name (must be spelled exactly as when created)
+ :type aspect_name: str
+ :returns: int
+ """
+ id = -1
+ aspects = self._connection.getUserData()['aspects']
+ for aspect in aspects:
+ if aspect['name'] == aspect_name: id = aspect['id']
+ return id
+
+ def filter(self, ids):
+ """Filters posts by given aspect ids.
+
+ :parameter ids: list of apsect ids
+ :type ids: list of integers
+ """
+ self._location = 'aspects.json?a_ids[]=' + '{}'.format('&a_ids[]='.join(ids))
+ self.fill() # this will create entirely new list of posts.
+
+ def add(self, aspect_name, visible=0):
+ """This function adds a new aspect.
+ Status code 422 is accepted because it is returned by D* when
+ you try to add aspect already present on your aspect list.
+
+ :param aspect_name: name of aspect to create
+ :param visible: whether the contacts in this aspect are visible to each other or not
+
+ :returns: Aspect() object of just created aspect
+ """
+ data = {'authenticity_token': repr(self._connection),
+ 'aspect[name]': aspect_name,
+ 'aspect[contacts_visible]': visible}
+
+ request = self._connection.post('aspects', data=data)
+ if request.status_code not in [200, 422]:
+ raise Exception('wrong status code: {0}'.format(request.status_code))
+
+ id = self.getAspectID(aspect_name)
+ return Aspect(self._connection, id)
+
+ def remove(self, id=-1, name=''):
+ """This method removes an aspect.
+ You can give it either id or name of the aspect.
+ When both are specified, id takes precedence over name.
+
+ Status code 500 is accepted because although the D* will
+ go nuts it will remove the aspect anyway.
+
+ :param aspect_id: id fo aspect to remove
+ :type aspect_id: int
+ :param name: name of aspect to remove
+ :type name: str
+ """
+ if id == -1 and name: id = self.getAspectID(name)
+ data = {'_method': 'delete',
+ 'authenticity_token': repr(self._connection)}
+ request = self._connection.post('aspects/{0}'.format(id), data=data)
+ if request.status_code not in [200, 302, 500]:
+ raise Exception('wrong status code: {0}: cannot remove aspect'.format(request.status_code))
class Commented(Generic):
- """This stream contains all posts
- the user has made a comment on.
- """
- _location = 'commented.json'
+ """This stream contains all posts
+ the user has made a comment on.
+ """
+ _location = 'commented.json'
class Liked(Generic):
- """This stream contains all posts the user liked.
- """
- _location = 'liked.json'
+ """This stream contains all posts the user liked.
+ """
+ _location = 'liked.json'
class Mentions(Generic):
- """This stream contains all posts
- the user is mentioned in.
- """
- _location = 'mentions.json'
+ """This stream contains all posts
+ the user is mentioned in.
+ """
+ _location = 'mentions.json'
class FollowedTags(Generic):
- """This stream contains all posts
- containing tags the user is following.
- """
- _location = 'followed_tags.json'
-
- def get(self):
- """Returns list of followed tags.
- """
- return []
-
- def remove(self, tag_id):
- """Stop following a tag.
-
- :param tag_id: tag id
- :type tag_id: int
- """
- data = {'authenticity_token': self._connection.get_token()}
- request = self._connection.delete('tag_followings/{0}'.format(tag_id), data=data)
- if request.status_code != 404:
- raise Exception('wrong status code: {0}'.format(request.status_code))
-
- def add(self, tag_name):
- """Follow new tag.
- Error code 403 is accepted because pods respod with it when request
- is sent to follow a tag that a user already follows.
-
- :param tag_name: tag name
- :type tag_name: str
- :returns: int (response code)
- """
- data = {'name': tag_name,
- 'authenticity_token': repr(self._connection),
- }
- headers = {'content-type': 'application/json',
- 'x-csrf-token': repr(self._connection),
- 'accept': 'application/json'
- }
-
- request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
-
- if request.status_code not in [201, 403]:
- raise Exception('wrong error code: {0}'.format(request.status_code))
- return request.status_code
+ """This stream contains all posts
+ containing tags the user is following.
+ """
+ _location = 'followed_tags.json'
+
+ def get(self):
+ """Returns list of followed tags.
+ """
+ return []
+
+ def remove(self, tag_id):
+ """Stop following a tag.
+
+ :param tag_id: tag id
+ :type tag_id: int
+ """
+ data = {'authenticity_token': self._connection.get_token()}
+ request = self._connection.delete('tag_followings/{0}'.format(tag_id), data=data)
+ if request.status_code != 404:
+ raise Exception('wrong status code: {0}'.format(request.status_code))
+
+ def add(self, tag_name):
+ """Follow new tag.
+ Error code 403 is accepted because pods respod with it when request
+ is sent to follow a tag that a user already follows.
+
+ :param tag_name: tag name
+ :type tag_name: str
+ :returns: int (response code)
+ """
+ data = {'name': tag_name,
+ 'authenticity_token': repr(self._connection),
+ }
+ headers = {'content-type': 'application/json',
+ 'x-csrf-token': repr(self._connection),
+ 'accept': 'application/json'
+ }
+
+ request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
+
+ if request.status_code not in [201, 403]:
+ raise Exception('wrong error code: {0}'.format(request.status_code))
+ return request.status_code
class Tag(Generic):
- """This stream contains all posts containing a tag.
- """
- def __init__(self, connection, tag, fetch=True):
- """
- :param connection: Connection() object
- :type connection: diaspy.connection.Connection
- :param tag: tag name
- :type tag: str
- """
- self._connection = connection
- self._location = 'tags/{0}.json'.format(tag)
- if fetch: self.fill()
+ """This stream contains all posts containing a tag.
+ """
+ def __init__(self, connection, tag, fetch=True):
+ """
+ :param connection: Connection() object
+ :type connection: diaspy.connection.Connection
+ :param tag: tag name
+ :type tag: str
+ """
+ self._connection = connection
+ self._location = 'tags/{0}.json'.format(tag)
+ if fetch: self.fill()