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.
58 if max_time
: params
['max_time'] = max_time
59 request
= self
._connection
.get(self
._location
, params
=params
)
60 if request
.status_code
!= 200:
61 raise errors
.StreamError('wrong status code: {0}'.format(request
.status_code
))
62 return [Post(self
._connection
, post
['id']) for post
in request
.json()]
64 def _expand(self
, new_stream
):
65 """Appends older posts to stream.
67 ids
= [post
.post_id
for post
in self
._stream
]
69 for post
in new_stream
:
70 if post
.post_id
not in ids
:
72 ids
.append(post
.post_id
)
75 def _update(self
, new_stream
):
76 """Updates stream with new posts.
78 ids
= [post
.id for post
in self
._stream
]
81 for i
in range(len(new_stream
)):
82 if new_stream
[-i
].id not in ids
:
83 stream
= [new_stream
[-i
]] + stream
84 ids
.append(new_stream
[-i
].id)
88 """Removes all posts from stream.
93 """Removes all unexistent posts from stream.
96 for post
in self
._stream
:
104 if not deleted
: stream
.append(post
)
105 self
._stream
= stream
110 self
._update
(self
._obtain
())
113 """Fills the stream with posts.
115 self
._stream
= self
._obtain
()
117 def more(self
, max_time
=0):
118 """Tries to download more (older ones) Posts from Stream.
120 :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
123 if not max_time
: max_time
= self
.max_time
- 3000000
124 self
.max_time
= max_time
125 new_stream
= self
._obtain
(max_time
=max_time
)
126 self
._expand
(new_stream
)
129 class Outer(Generic
):
130 """Object used by diaspy.models.User to represent
131 stream of other user.
134 """Obtains stream of other user.
136 request
= self
._connection
.get(self
._location
)
137 if request
.status_code
!= 200:
138 raise error
.StreamError('wrong status code: {0}'.format(request
.status_code
))
139 return [Post(self
._connection
, post
['id']) for post
in request
.json()]
142 class Stream(Generic
):
143 """The main stream containing the combined posts of the
144 followed users and tags and the community spotlights posts
145 if the user enabled those.
147 location
= 'stream.json'
149 def post(self
, text
='', aspect_ids
='public', photos
=None, photo
=''):
150 """This function sends a post to an aspect.
151 If both `photo` and `photos` are specified `photos` takes precedence.
153 :param text: Text to post.
155 :param aspect_ids: Aspect ids to send post to.
156 :type aspect_ids: str
157 :param photo: filename of photo to post
159 :param photos: id of photo to post (obtained from _photoupload())
162 :returns: diaspy.models.Post -- the Post which has been created
165 data
['aspect_ids'] = aspect_ids
166 data
['status_message'] = {'text': text
}
167 if photo
: data
['photos'] = self
._photoupload
(photo
)
168 if photos
: data
['photos'] = photos
170 request
= self
._connection
.post('status_messages',
171 data
=json
.dumps(data
),
172 headers
={'content-type': 'application/json',
173 'accept': 'application/json',
174 'x-csrf-token': repr(self
._connection
)})
175 if request
.status_code
!= 201:
176 raise Exception('{0}: Post could not be posted.'.format(request
.status_code
))
178 post
= Post(self
._connection
, request
.json()['id'])
181 def _photoupload(self
, filename
):
182 """Uploads picture to the pod.
184 :param filename: path to picture file
187 :returns: id of the photo being uploaded
189 data
= open(filename
, 'rb')
194 params
['photo[pending]'] = 'true'
195 params
['set_profile_image'] = ''
196 params
['qqfile'] = filename
197 aspects
= self
._connection
.getUserInfo()['aspects']
198 for i
, aspect
in enumerate(aspects
):
199 params
['photo[aspect_ids][{0}]'.format(i
)] = aspect
['id']
201 headers
= {'content-type': 'application/octet-stream',
202 'x-csrf-token': repr(self
._connection
),
203 'x-file-name': filename
}
205 request
= self
._connection
.post('photos', data
=image
, params
=params
, headers
=headers
)
206 if request
.status_code
!= 200:
207 raise Exception('wrong error code: {0}'.format(request
.status_code
))
208 return request
.json()['data']['photo']['id']
211 class Activity(Stream
):
212 """Stream representing user's activity.
214 _location
= 'activity.json'
216 def _delid(self
, id):
217 """Deletes post with given id.
220 for p
in self
._stream
:
224 if post
is not None: post
.delete()
226 def delete(self
, post
):
227 """Deletes post from users activity.
228 `post` can be either post id or Post()
229 object which will be identified and deleted.
230 After deleting post the stream will be filled.
232 :param post: post identifier
233 :type post: str, diaspy.models.Post
235 if type(post
) == str: self
._delid
(post
)
236 elif type(post
) == Post
: post
.delete()
237 else: raise TypeError('this method accepts str or Post types: {0} given')
241 class Aspects(Generic
):
242 """This stream contains the posts filtered by
243 the specified aspect IDs. You can choose the aspect IDs with
244 the parameter `aspect_ids` which value should be
245 a comma seperated list of aspect IDs.
246 If the parameter is ommitted all aspects are assumed.
247 An example call would be `aspects.json?aspect_ids=23,5,42`
249 _location
= 'aspects.json'
251 def getAspectID(self
, aspect_name
):
252 """Returns id of an aspect of given name.
253 Returns -1 if aspect is not found.
255 :param aspect_name: aspect name (must be spelled exactly as when created)
256 :type aspect_name: str
260 aspects
= self
._connection
.getUserInfo()['aspects']
261 for aspect
in aspects
:
262 if aspect
['name'] == aspect_name
: id = aspect
['id']
265 def filterByIDs(self
, ids
):
266 self
._location
+= '?{0}'.format(','.join(ids
))
269 def add(self
, aspect_name
, visible
=0):
270 """This function adds a new aspect.
271 Status code 422 is accepted because it is returned by D* when
272 you try to add aspect already present on your aspect list.
274 :returns: Aspect() object of just created aspect
276 data
= {'authenticity_token': self
._connection
.get_token(),
277 'aspect[name]': aspect_name
,
278 'aspect[contacts_visible]': visible
}
280 request
= self
._connection
.post('aspects', data
=data
)
281 if request
.status_code
not in [200, 422]:
282 raise Exception('wrong status code: {0}'.format(request
.status_code
))
284 id = self
.getAspectID(aspect_name
)
285 return Aspect(self
._connection
, id)
287 def remove(self
, aspect_id
=-1, name
=''):
288 """This method removes an aspect.
289 You can give it either id or name of the aspect.
290 When both are specified, id takes precedence over name.
292 Status code 500 is accepted because although the D* will
293 go nuts it will remove the aspect anyway.
295 :param aspect_id: id fo aspect to remove
297 :param name: name of aspect to remove
300 if aspect_id
== -1 and name
: aspect_id
= self
.getAspectID(name
)
301 data
= {'_method': 'delete',
302 'authenticity_token': self
._connection
.get_token()}
303 request
= self
._connection
.post('aspects/{0}'.format(aspect_id
), data
=data
)
304 if request
.status_code
not in [200, 302, 500]:
305 raise Exception('wrong status code: {0}: cannot remove aspect'.format(request
.status_code
))
308 class Commented(Generic
):
309 """This stream contains all posts
310 the user has made a comment on.
312 _location
= 'commented.json'
315 class Liked(Generic
):
316 """This stream contains all posts the user liked.
318 _location
= 'liked.json'
321 class Mentions(Generic
):
322 """This stream contains all posts
323 the user is mentioned in.
325 _location
= 'mentions.json'
328 class FollowedTags(Generic
):
329 """This stream contains all posts
330 containing tags the user is following.
332 _location
= 'followed_tags.json'
334 def remove(self
, tag_id
):
335 """Stop following a tag.
337 :param tag_id: tag id
340 data
= {'authenticity_token': self
._connection
.get_token()}
341 request
= self
._connection
.delete('tag_followings/{0}'.format(tag_id
), data
=data
)
342 if request
.status_code
!= 404:
343 raise Exception('wrong status code: {0}'.format(request
.status_code
))
345 def add(self
, tag_name
):
347 Error code 403 is accepted because pods respod with it when request
348 is sent to follow a tag that a user already follows.
350 :param tag_name: tag name
352 :returns: int (response code)
354 data
= {'name': tag_name
,
355 'authenticity_token': self
._connection
.get_token(),
357 headers
= {'content-type': 'application/json',
358 'x-csrf-token': self
._connection
.get_token(),
359 'accept': 'application/json'
362 request
= self
._connection
.post('tag_followings', data
=json
.dumps(data
), headers
=headers
)
364 if request
.status_code
not in [201, 403]:
365 raise Exception('wrong error code: {0}'.format(request
.status_code
))
366 return request
.status_code
370 """This stream contains all posts containing a tag.
372 def __init__(self
, connection
, tag
):
374 :param connection: Connection() object
375 :type connection: diaspy.connection.Connection
379 self
._connection
= connection
380 self
._location
= 'tags/{0}.json'.format(tag
)