1 """Docstrings for this module are taken from:
2 https://gist.github.com/MrZYX/01c93096c30dc44caf71
4 Documentation for D* JSON API taken from:
5 http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
11 from diaspy
.models
import Post
, Aspect
12 from diaspy
import errors
16 """Object representing generic stream.
18 _location
= 'stream.json'
20 def __init__(self
, connection
, location
=''):
22 :param connection: Connection() object
23 :type connection: diaspy.connection.Connection
24 :param location: location of json (optional)
27 self
._connection
= connection
28 if location
: self
._location
= location
31 self
.max_time
= int(time
.mktime(time
.gmtime()))
34 def __contains__(self
, post
):
35 """Returns True if stream contains given post.
37 return post
in self
._stream
40 """Provides iterable interface for stream.
42 return iter(self
._stream
)
44 def __getitem__(self
, n
):
45 """Returns n-th item in Stream.
47 return self
._stream
[n
]
50 """Returns length of the Stream.
52 return len(self
._stream
)
54 def _obtain(self
, max_time
=0):
55 """Obtains stream from pod.
59 params
['max_time'] = max_time
60 params
['_'] = self
.max_time
61 request
= self
._connection
.get(self
._location
, params
=params
)
62 if request
.status_code
!= 200:
63 raise errors
.StreamError('wrong status code: {0}'.format(request
.status_code
))
64 return [Post(self
._connection
, post
['id']) for post
in request
.json()]
66 def _expand(self
, new_stream
):
67 """Appends older posts to stream.
69 ids
= [post
.id for post
in self
._stream
]
71 for post
in new_stream
:
72 if post
.id not in ids
:
77 def _update(self
, new_stream
):
78 """Updates stream with new posts.
80 ids
= [post
.id for post
in self
._stream
]
83 for i
in range(len(new_stream
)):
84 if new_stream
[-i
].id not in ids
:
85 stream
= [new_stream
[-i
]] + stream
86 ids
.append(new_stream
[-i
].id)
90 """Removes all posts from stream.
95 """Removes all unexistent posts from stream.
98 for post
in self
._stream
:
106 if not deleted
: stream
.append(post
)
107 self
._stream
= stream
112 self
._update
(self
._obtain
())
115 """Fills the stream with posts.
117 self
._stream
= self
._obtain
()
119 def more(self
, max_time
=0):
120 """Tries to download more (older ones) Posts from Stream.
122 :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
125 if not max_time
: max_time
= self
.max_time
- 3000000
126 self
.max_time
= max_time
127 new_stream
= self
._obtain
(max_time
=max_time
)
128 self
._expand
(new_stream
)
131 """Fetches full stream - containing all posts.
132 WARNING: this can be a **VERY** time consuming function on slow connections of massive streams.
134 :returns: integer, lenght of the stream
136 oldstream
= self
.copy()
138 while len(oldstream
) != len(self
):
139 oldstream
= self
.copy()
144 """Returns copy (list of posts) of current stream.
146 return [p
for p
in self
._stream
]
148 def json(self
, comments
=False):
149 """Returns JSON encoded string containing stream's data.
151 :param comments: to include comments or not to include 'em, that is the question this param holds answer to
154 stream
= [post
for post
in self
._stream
]
156 for i
, post
in enumerate(stream
):
157 post
._fetchcomments
()
158 comments
= [c
.data
for c
in post
.comments
]
159 post
['interactions']['comments'] = comments
161 stream
= [post
.data
for post
in stream
]
162 return json
.dumps(stream
)
165 class Outer(Generic
):
166 """Object used by diaspy.models.User to represent
167 stream of other user.
169 def _obtain(self
, max_time
=0):
170 """Obtains stream from pod.
173 if max_time
: params
['max_time'] = max_time
174 request
= self
._connection
.get(self
._location
, params
=params
)
175 if request
.status_code
!= 200:
176 raise errors
.StreamError('wrong status code: {0}'.format(request
.status_code
))
177 return [Post(self
._connection
, post
['id']) for post
in request
.json()]
180 class Stream(Generic
):
181 """The main stream containing the combined posts of the
182 followed users and tags and the community spotlights posts
183 if the user enabled those.
185 location
= 'stream.json'
187 def post(self
, text
='', aspect_ids
='public', photos
=None, photo
=''):
188 """This function sends a post to an aspect.
189 If both `photo` and `photos` are specified `photos` takes precedence.
191 :param text: Text to post.
193 :param aspect_ids: Aspect ids to send post to.
194 :type aspect_ids: str
195 :param photo: filename of photo to post
197 :param photos: id of photo to post (obtained from _photoupload())
200 :returns: diaspy.models.Post -- the Post which has been created
203 data
['aspect_ids'] = aspect_ids
204 data
['status_message'] = {'text': text
}
205 if photo
: data
['photos'] = self
._photoupload
(photo
)
206 if photos
: data
['photos'] = photos
208 request
= self
._connection
.post('status_messages',
209 data
=json
.dumps(data
),
210 headers
={'content-type': 'application/json',
211 'accept': 'application/json',
212 'x-csrf-token': repr(self
._connection
)})
213 if request
.status_code
!= 201:
214 raise Exception('{0}: Post could not be posted.'.format(request
.status_code
))
216 post
= Post(self
._connection
, request
.json()['id'])
219 def _photoupload(self
, filename
):
220 """Uploads picture to the pod.
222 :param filename: path to picture file
225 :returns: id of the photo being uploaded
227 data
= open(filename
, 'rb')
232 params
['photo[pending]'] = 'true'
233 params
['set_profile_image'] = ''
234 params
['qqfile'] = filename
235 aspects
= self
._connection
.getUserInfo()['aspects']
236 for i
, aspect
in enumerate(aspects
):
237 params
['photo[aspect_ids][{0}]'.format(i
)] = aspect
['id']
239 headers
= {'content-type': 'application/octet-stream',
240 'x-csrf-token': repr(self
._connection
),
241 'x-file-name': filename
}
243 request
= self
._connection
.post('photos', data
=image
, params
=params
, headers
=headers
)
244 if request
.status_code
!= 200:
245 raise errors
.StreamError('photo cannot be uploaded: {0}'.format(request
.status_code
))
246 return request
.json()['data']['photo']['id']
249 class Activity(Stream
):
250 """Stream representing user's activity.
252 _location
= 'activity.json'
254 def _delid(self
, id):
255 """Deletes post with given id.
258 for p
in self
._stream
:
262 if post
is not None: post
.delete()
264 def delete(self
, post
):
265 """Deletes post from users activity.
266 `post` can be either post id or Post()
267 object which will be identified and deleted.
268 After deleting post the stream will be filled.
270 :param post: post identifier
271 :type post: str, diaspy.models.Post
273 if type(post
) == str: self
._delid
(post
)
274 elif type(post
) == Post
: post
.delete()
275 else: raise TypeError('this method accepts str or Post types: {0} given')
279 class Aspects(Generic
):
280 """This stream contains the posts filtered by
281 the specified aspect IDs. You can choose the aspect IDs with
282 the parameter `aspect_ids` which value should be
283 a comma seperated list of aspect IDs.
284 If the parameter is ommitted all aspects are assumed.
285 An example call would be `aspects.json?aspect_ids=23,5,42`
287 _location
= 'aspects.json'
289 def getAspectID(self
, aspect_name
):
290 """Returns id of an aspect of given name.
291 Returns -1 if aspect is not found.
293 :param aspect_name: aspect name (must be spelled exactly as when created)
294 :type aspect_name: str
298 aspects
= self
._connection
.getUserInfo()['aspects']
299 for aspect
in aspects
:
300 if aspect
['name'] == aspect_name
: id = aspect
['id']
303 def filterByIDs(self
, ids
):
304 self
._location
+= '?{0}'.format(','.join(ids
))
307 def add(self
, aspect_name
, visible
=0):
308 """This function adds a new aspect.
309 Status code 422 is accepted because it is returned by D* when
310 you try to add aspect already present on your aspect list.
312 :returns: Aspect() object of just created aspect
314 data
= {'authenticity_token': self
._connection
.get_token(),
315 'aspect[name]': aspect_name
,
316 'aspect[contacts_visible]': visible
}
318 request
= self
._connection
.post('aspects', data
=data
)
319 if request
.status_code
not in [200, 422]:
320 raise Exception('wrong status code: {0}'.format(request
.status_code
))
322 id = self
.getAspectID(aspect_name
)
323 return Aspect(self
._connection
, id)
325 def remove(self
, aspect_id
=-1, name
=''):
326 """This method removes an aspect.
327 You can give it either id or name of the aspect.
328 When both are specified, id takes precedence over name.
330 Status code 500 is accepted because although the D* will
331 go nuts it will remove the aspect anyway.
333 :param aspect_id: id fo aspect to remove
335 :param name: name of aspect to remove
338 if aspect_id
== -1 and name
: aspect_id
= self
.getAspectID(name
)
339 data
= {'_method': 'delete',
340 'authenticity_token': self
._connection
.get_token()}
341 request
= self
._connection
.post('aspects/{0}'.format(aspect_id
), data
=data
)
342 if request
.status_code
not in [200, 302, 500]:
343 raise Exception('wrong status code: {0}: cannot remove aspect'.format(request
.status_code
))
346 class Commented(Generic
):
347 """This stream contains all posts
348 the user has made a comment on.
350 _location
= 'commented.json'
353 class Liked(Generic
):
354 """This stream contains all posts the user liked.
356 _location
= 'liked.json'
359 class Mentions(Generic
):
360 """This stream contains all posts
361 the user is mentioned in.
363 _location
= 'mentions.json'
366 class FollowedTags(Generic
):
367 """This stream contains all posts
368 containing tags the user is following.
370 _location
= 'followed_tags.json'
372 def remove(self
, tag_id
):
373 """Stop following a tag.
375 :param tag_id: tag id
378 data
= {'authenticity_token': self
._connection
.get_token()}
379 request
= self
._connection
.delete('tag_followings/{0}'.format(tag_id
), data
=data
)
380 if request
.status_code
!= 404:
381 raise Exception('wrong status code: {0}'.format(request
.status_code
))
383 def add(self
, tag_name
):
385 Error code 403 is accepted because pods respod with it when request
386 is sent to follow a tag that a user already follows.
388 :param tag_name: tag name
390 :returns: int (response code)
392 data
= {'name': tag_name
,
393 'authenticity_token': self
._connection
.get_token(),
395 headers
= {'content-type': 'application/json',
396 'x-csrf-token': self
._connection
.get_token(),
397 'accept': 'application/json'
400 request
= self
._connection
.post('tag_followings', data
=json
.dumps(data
), headers
=headers
)
402 if request
.status_code
not in [201, 403]:
403 raise Exception('wrong error code: {0}'.format(request
.status_code
))
404 return request
.status_code
408 """This stream contains all posts containing a tag.
410 def __init__(self
, connection
, tag
):
412 :param connection: Connection() object
413 :type connection: diaspy.connection.Connection
417 self
._connection
= connection
418 self
._location
= 'tags/{0}.json'.format(tag
)