-import json
-from diaspy.models import Post
-
"""Docstrings for this module are taken from:
https://gist.github.com/MrZYX/01c93096c30dc44caf71
http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
"""
-class Generic:
- """Object representing generic stream. Used in Tag(),
- Stream(), Activity() etc.
+
+import json
+import time
+from diaspy.models import Post, Aspect
+from diaspy import errors
+
+
+class Generic():
+ """Object representing generic stream.
"""
- def __init__(self, connection, location=''):
+ _location = 'stream.json'
+
+ def __init__(self, connection, location='', fetch=True):
"""
:param connection: Connection() object
:type connection: diaspy.connection.Connection
- :param location: location of json
+ :param location: location of json (optional)
:type location: str
+ :param fetch: will call .fill() if true
+ :type fetch: bool
"""
self._connection = connection
- self._setlocation()
if location: self._location = location
self._stream = []
- self.fill()
+ # 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.
"""
- if type(post) is not Post:
- raise TypeError('stream can contain only posts: checked for {0}'.format(type(post)))
return post in self._stream
def __iter__(self):
"""
return len(self._stream)
- def _setlocation(self):
- """Sets location of the stream.
- Location defaults to 'stream.json'
-
- NOTICE: inheriting objects should override this method
- and set their own value to the `location`.
- However, it is possible to override default location by
- passing the desired one to the constructor.
- For example:
-
- def _setlocation(self):
- self._location = 'foo.json'
-
-
- :param location: url of the stream
- :type location: str
+ def _obtain(self, max_time=0):
+ """Obtains stream from pod.
+ """
+ params = {}
+ if max_time:
+ params['max_time'] = max_time
+ params['_'] = int(time.time() * 1000)
+ request = self._connection.get(self._location, params=params, headers={'cookie': ''})
+ if request.status_code != 200:
+ raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ return [Post(self._connection, post['id']) for post in request.json()]
- :returns: str
+ def _expand(self, new_stream):
+ """Appends older posts to stream.
"""
- self._location = 'stream.json'
+ ids = [post.id for post in self._stream]
+ stream = self._stream
+ for post in new_stream:
+ if post.id not in ids:
+ stream.append(post)
+ ids.append(post.id)
+ self._stream = stream
- def _obtain(self):
- """Obtains stream from pod.
+ def _update(self, new_stream):
+ """Updates stream with new posts.
"""
- request = self._connection.get(self._location)
- if request.status_code != 200:
- raise Exception('wrong status code: {0}'.format(request.status_code))
- return [Post(str(post['id']), self._connection) for post in request.json()]
+ ids = [post.id for post in self._stream]
+
+ stream = self._stream
+ for i in range(len(new_stream)):
+ if new_stream[-i].id not in ids:
+ stream = [new_stream[-i]] + stream
+ ids.append(new_stream[-i].id)
+ self._stream = stream
def clear(self):
- """Removes all posts from stream.
+ """Set stream to empty.
"""
self._stream = []
for post in self._stream:
deleted = False
try:
- post.get_data()
+ post.update()
stream.append(post)
except Exception:
deleted = True
self._stream = stream
def update(self):
- """Updates stream.
+ """Updates stream with new posts.
"""
- new_stream = self._obtain()
- ids = [post.post_id for post in self._stream]
-
- stream = self._stream
- for i in range(len(new_stream)):
- if new_stream[-i].post_id not in ids:
- stream = [new_stream[-i]] + stream
- ids.append(new_stream[-i].post_id)
-
- self._stream = stream
+ 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=84600):
+ """Tries to download more (older posts) posts from Stream.
+
+ :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 - backtime
+ self.max_time = max_time
+ new_stream = self._obtain(max_time=max_time)
+ self._expand(new_stream)
+
+ def full(self, backtime=84600, 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):
+ """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)
+
class Outer(Generic):
"""Object used by diaspy.models.User to represent
stream of other user.
"""
- def _obtain(self):
- """Obtains stream of other user.
+ def _obtain(self, max_time=0):
+ """Obtains stream from pod.
"""
- request = self._connection.get(self._location)
+ params = {}
+ if max_time: params['max_time'] = max_time
+ request = self._connection.get(self._location, params=params)
if request.status_code != 200:
- raise Exception('wrong status code: {0}'.format(request.status_code))
- return [Post(str(post['id']), self._connection) for post in request.json()]
+ raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ return [Post(self._connection, post['id']) for post in request.json()]
class Stream(Generic):
- """The main stream containing the combined posts of the
- followed users and tags and the community spotlights posts
+ """The main stream containing the combined posts of the
+ followed users and tags and the community spotlights posts
if the user enabled those.
"""
- def _setlocation(self):
- self._location = 'stream.json'
+ location = 'stream.json'
- def post(self, text, aspect_ids='public', photos=None):
- """This function sends a post to an aspect
+ def post(self, text='', aspect_ids='public', photos=None, photo='', 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
:returns: diaspy.models.Post -- the Post which has been created
"""
data = {}
data['aspect_ids'] = aspect_ids
- data['status_message'] = {'text': text}
+ data['status_message'] = {'text': text, 'provider_display_name': provider_display_name}
+ if photo: data['photos'] = self._photoupload(photo)
if photos: data['photos'] = photos
+
request = self._connection.post('status_messages',
data=json.dumps(data),
headers={'content-type': 'application/json',
'accept': 'application/json',
- 'x-csrf-token': self._connection.get_token()})
+ 'x-csrf-token': repr(self._connection)})
if request.status_code != 201:
- raise Exception('{0}: Post could not be posted.'.format(
- request.status_code))
-
- post = Post(str(request.json()['id']), self._connection)
+ raise Exception('{0}: Post could not be posted.'.format(request.status_code))
+ post = Post(self._connection, request.json()['id'])
return post
- def post_picture(self, filename):
- """This method posts a picture to D*.
+ def _photoupload(self, filename):
+ """Uploads picture to the pod.
- :param filename: Path to picture file.
+ :param filename: path to picture file
:type filename: str
+
+ :returns: id of the photo being uploaded
"""
data = open(filename, 'rb')
image = data.read()
params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
headers = {'content-type': 'application/octet-stream',
- 'x-csrf-token': self._connection.get_token(),
+ '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 Exception('wrong error code: {0}'.format())
- return request
+ raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
+ return request.json()['data']['photo']['id']
-class Activity(Generic):
+class Activity(Stream):
"""Stream representing user's activity.
"""
- def _setlocation(self):
- self._location = 'activity.json'
+ _location = 'activity.json'
def _delid(self, id):
"""Deletes post with given id.
"""
if type(post) == str: self._delid(post)
elif type(post) == Post: post.delete()
- else:
- raise TypeError('this method accepts only int, str or Post: {0} given')
+ else: raise TypeError('this method accepts str or Post types: {0} given')
self.fill()
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.
+ """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`
"""
- def _setlocation(self):
- self._location = 'aspects.json'
+ _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.getUserInfo()['aspects']
+ for aspect in aspects:
+ if aspect['name'] == aspect_name: id = aspect['id']
+ return id
+
+ def filterByIDs(self, ids):
+ self._location += '?{0}'.format(','.join(ids))
+ self.fill()
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': self._connection.get_token(),
+ data = {'authenticity_token': repr(self._connection),
'aspect[name]': aspect_name,
'aspect[contacts_visible]': visible}
- r = self._connection.post('aspects', data=data)
- if r.status_code != 200:
- raise Exception('wrong status code: {0}'.format(r.status_code))
+ 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))
- def remove(self, aspect_id):
+ 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
"""
- data = {'authenticity_token': self.connection.get_token()}
- r = self.connection.delete('aspects/{}'.format(aspect_id),
- data=data)
- if r.status_code != 404:
- raise Exception('wrong status code: {0}'.format(r.status_code))
+ 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
+ """This stream contains all posts
the user has made a comment on.
"""
- def _setlocation(self):
- self._location = 'commented.json'
+ _location = 'commented.json'
class Liked(Generic):
"""This stream contains all posts the user liked.
"""
- def _setlocation(self):
- self._location = 'liked.json'
+ _location = 'liked.json'
class Mentions(Generic):
- """This stream contains all posts
+ """This stream contains all posts
the user is mentioned in.
"""
- def _setlocation(self):
- self._location = 'mentions.json'
+ _location = 'mentions.json'
class FollowedTags(Generic):
- """This stream contains all posts
+ """This stream contains all posts
containing tags the user is following.
"""
- def _setlocation(self):
- self._location = 'followed_tags.json'
+ _location = 'followed_tags.json'
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()}
+ 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
+ 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':self._connection.get_token(),
- }
- headers={'content-type': 'application/json',
- 'x-csrf-token': self._connection.get_token(),
- 'accept': 'application/json'}
+ 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):
+ """
+ :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)
+ self.fill()