pydoc will now correctly read doctsing for diaspy/streams.py,
[diaspy.git] / diaspy / streams.py
CommitLineData
505fc964
MM
1"""Docstrings for this module are taken from:
2https://gist.github.com/MrZYX/01c93096c30dc44caf71
3
4Documentation for D* JSON API taken from:
5http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
6"""
1232dac5 7
f605e88d 8
4b1645dc
MM
9import json
10import time
11from diaspy.models import Post, Aspect
12from diaspy import errors
13
14
7a818fdb
MM
15class Generic():
16 """Object representing generic stream.
1232dac5 17 """
27a28aaf 18 _location = 'stream.json'
27a28aaf 19
505fc964 20 def __init__(self, connection, location=''):
1232dac5
MM
21 """
22 :param connection: Connection() object
505fc964 23 :type connection: diaspy.connection.Connection
27a28aaf 24 :param location: location of json (optional)
505fc964 25 :type location: str
1232dac5
MM
26 """
27 self._connection = connection
505fc964 28 if location: self._location = location
63f1d9f1
MM
29 self._stream = []
30 # since epoch
31 self.max_time = int(time.mktime(time.gmtime()))
1232dac5
MM
32 self.fill()
33
34 def __contains__(self, post):
35 """Returns True if stream contains given post.
36 """
1232dac5
MM
37 return post in self._stream
38
39 def __iter__(self):
40 """Provides iterable interface for stream.
41 """
42 return iter(self._stream)
43
44 def __getitem__(self, n):
45 """Returns n-th item in Stream.
46 """
47 return self._stream[n]
48
49 def __len__(self):
50 """Returns length of the Stream.
51 """
52 return len(self._stream)
53
ed366d44 54 def _obtain(self, max_time=0):
1232dac5
MM
55 """Obtains stream from pod.
56 """
ed366d44
MM
57 params = {}
58 if max_time: params['max_time'] = max_time
59 request = self._connection.get(self._location, params=params)
1232dac5 60 if request.status_code != 200:
4b1645dc 61 raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
78cc478a 62 return [Post(self._connection, post['id']) for post in request.json()]
1232dac5 63
ed366d44
MM
64 def _expand(self, new_stream):
65 """Appends older posts to stream.
66 """
67 ids = [post.post_id for post in self._stream]
68 stream = self._stream
69 for post in new_stream:
70 if post.post_id not in ids:
71 stream.append(post)
72 ids.append(post.post_id)
73 self._stream = stream
74
75 def _update(self, new_stream):
76 """Updates stream with new posts.
77 """
78cc478a 78 ids = [post.id for post in self._stream]
ed366d44
MM
79
80 stream = self._stream
81 for i in range(len(new_stream)):
78cc478a 82 if new_stream[-i].id not in ids:
ed366d44 83 stream = [new_stream[-i]] + stream
9aa1c960 84 ids.append(new_stream[-i].id)
ed366d44
MM
85 self._stream = stream
86
1232dac5
MM
87 def clear(self):
88 """Removes all posts from stream.
89 """
90 self._stream = []
91
505fc964
MM
92 def purge(self):
93 """Removes all unexistent posts from stream.
94 """
95 stream = []
96 for post in self._stream:
97 deleted = False
98 try:
78cc478a 99 post.update()
505fc964
MM
100 stream.append(post)
101 except Exception:
102 deleted = True
103 finally:
104 if not deleted: stream.append(post)
105 self._stream = stream
106
1232dac5
MM
107 def update(self):
108 """Updates stream.
109 """
3cf4514e 110 self._update(self._obtain())
1232dac5
MM
111
112 def fill(self):
113 """Fills the stream with posts.
114 """
115 self._stream = self._obtain()
116
178faa46 117 def more(self, max_time=0):
33b34938 118 """Tries to download more (older ones) Posts from Stream.
ed366d44
MM
119
120 :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
121 :type max_time: int
33b34938 122 """
178faa46
MM
123 if not max_time: max_time = self.max_time - 3000000
124 self.max_time = max_time
ed366d44
MM
125 new_stream = self._obtain(max_time=max_time)
126 self._expand(new_stream)
33b34938 127
1232dac5 128
beaa09fb
MM
129class Outer(Generic):
130 """Object used by diaspy.models.User to represent
131 stream of other user.
132 """
133 def _obtain(self):
134 """Obtains stream of other user.
135 """
136 request = self._connection.get(self._location)
137 if request.status_code != 200:
78cc478a
MM
138 raise error.StreamError('wrong status code: {0}'.format(request.status_code))
139 return [Post(self._connection, post['id']) for post in request.json()]
beaa09fb
MM
140
141
1232dac5 142class Stream(Generic):
f605e88d
MM
143 """The main stream containing the combined posts of the
144 followed users and tags and the community spotlights posts
505fc964 145 if the user enabled those.
1232dac5 146 """
27a28aaf 147 location = 'stream.json'
505fc964 148
a98c6792
MM
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.
1232dac5
MM
152
153 :param text: Text to post.
154 :type text: str
155 :param aspect_ids: Aspect ids to send post to.
156 :type aspect_ids: str
a98c6792
MM
157 :param photo: filename of photo to post
158 :type photo: str
159 :param photos: id of photo to post (obtained from _photoupload())
160 :type photos: int
1232dac5
MM
161
162 :returns: diaspy.models.Post -- the Post which has been created
163 """
164 data = {}
165 data['aspect_ids'] = aspect_ids
166 data['status_message'] = {'text': text}
a98c6792 167 if photo: data['photos'] = self._photoupload(photo)
1232dac5 168 if photos: data['photos'] = photos
a98c6792 169
1232dac5
MM
170 request = self._connection.post('status_messages',
171 data=json.dumps(data),
172 headers={'content-type': 'application/json',
173 'accept': 'application/json',
78cc478a 174 'x-csrf-token': repr(self._connection)})
1232dac5 175 if request.status_code != 201:
a98c6792 176 raise Exception('{0}: Post could not be posted.'.format(request.status_code))
1232dac5 177
78cc478a 178 post = Post(self._connection, request.json()['id'])
1232dac5
MM
179 return post
180
66c3bb76
MM
181 def _photoupload(self, filename):
182 """Uploads picture to the pod.
1232dac5 183
66c3bb76 184 :param filename: path to picture file
1232dac5 185 :type filename: str
66c3bb76
MM
186
187 :returns: id of the photo being uploaded
1232dac5 188 """
38fabb63
MM
189 data = open(filename, 'rb')
190 image = data.read()
191 data.close()
192
1232dac5
MM
193 params = {}
194 params['photo[pending]'] = 'true'
195 params['set_profile_image'] = ''
196 params['qqfile'] = filename
38fabb63 197 aspects = self._connection.getUserInfo()['aspects']
1232dac5 198 for i, aspect in enumerate(aspects):
38fabb63 199 params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
1232dac5
MM
200
201 headers = {'content-type': 'application/octet-stream',
78cc478a 202 'x-csrf-token': repr(self._connection),
1232dac5 203 'x-file-name': filename}
38fabb63
MM
204
205 request = self._connection.post('photos', data=image, params=params, headers=headers)
206 if request.status_code != 200:
66c3bb76
MM
207 raise Exception('wrong error code: {0}'.format(request.status_code))
208 return request.json()['data']['photo']['id']
209
1232dac5 210
278febce 211class Activity(Stream):
1232dac5
MM
212 """Stream representing user's activity.
213 """
27a28aaf 214 _location = 'activity.json'
505fc964
MM
215
216 def _delid(self, id):
217 """Deletes post with given id.
218 """
219 post = None
220 for p in self._stream:
221 if p['id'] == id:
222 post = p
223 break
224 if post is not None: post.delete()
225
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.
231
232 :param post: post identifier
233 :type post: str, diaspy.models.Post
234 """
235 if type(post) == str: self._delid(post)
236 elif type(post) == Post: post.delete()
63f1d9f1 237 else: raise TypeError('this method accepts str or Post types: {0} given')
505fc964
MM
238 self.fill()
239
240
241class Aspects(Generic):
f605e88d
MM
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.
505fc964
MM
247 An example call would be `aspects.json?aspect_ids=23,5,42`
248 """
27a28aaf 249 _location = 'aspects.json'
27a28aaf 250
278febce 251 def getAspectID(self, aspect_name):
63cc182d
MM
252 """Returns id of an aspect of given name.
253 Returns -1 if aspect is not found.
254
278febce
MM
255 :param aspect_name: aspect name (must be spelled exactly as when created)
256 :type aspect_name: str
63cc182d
MM
257 :returns: int
258 """
259 id = -1
33b34938 260 aspects = self._connection.getUserInfo()['aspects']
278febce
MM
261 for aspect in aspects:
262 if aspect['name'] == aspect_name: id = aspect['id']
63cc182d
MM
263 return id
264
27a28aaf
MM
265 def filterByIDs(self, ids):
266 self._location += '?{0}'.format(','.join(ids))
267 self.fill()
268
505fc964
MM
269 def add(self, aspect_name, visible=0):
270 """This function adds a new aspect.
278febce 271 Status code 422 is accepted because it is returned by D* when
27a28aaf
MM
272 you try to add aspect already present on your aspect list.
273
d589deff 274 :returns: Aspect() object of just created aspect
505fc964 275 """
59ad210c 276 data = {'authenticity_token': self._connection.get_token(),
505fc964
MM
277 'aspect[name]': aspect_name,
278 'aspect[contacts_visible]': visible}
279
27a28aaf
MM
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))
283
278febce 284 id = self.getAspectID(aspect_name)
d589deff 285 return Aspect(self._connection, id)
505fc964 286
278febce 287 def remove(self, aspect_id=-1, name=''):
fbb19900 288 """This method removes an aspect.
278febce
MM
289 You can give it either id or name of the aspect.
290 When both are specified, id takes precedence over name.
291
292 Status code 500 is accepted because although the D* will
27a28aaf
MM
293 go nuts it will remove the aspect anyway.
294
295 :param aspect_id: id fo aspect to remove
296 :type aspect_id: int
1467ec15
MM
297 :param name: name of aspect to remove
298 :type name: str
fbb19900 299 """
278febce 300 if aspect_id == -1 and name: aspect_id = self.getAspectID(name)
27f09973 301 data = {'_method': 'delete',
dd0a4d9f
MM
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))
fbb19900 306
505fc964
MM
307
308class Commented(Generic):
f605e88d 309 """This stream contains all posts
505fc964
MM
310 the user has made a comment on.
311 """
27a28aaf 312 _location = 'commented.json'
505fc964
MM
313
314
315class Liked(Generic):
316 """This stream contains all posts the user liked.
317 """
27a28aaf 318 _location = 'liked.json'
505fc964
MM
319
320
321class Mentions(Generic):
f605e88d 322 """This stream contains all posts
505fc964
MM
323 the user is mentioned in.
324 """
27a28aaf 325 _location = 'mentions.json'
505fc964
MM
326
327
328class FollowedTags(Generic):
f605e88d 329 """This stream contains all posts
6c416e80 330 containing tags the user is following.
505fc964 331 """
27a28aaf 332 _location = 'followed_tags.json'
505fc964 333
a7c098e3 334 def remove(self, tag_id):
fbb19900
MM
335 """Stop following a tag.
336
a7c098e3
MM
337 :param tag_id: tag id
338 :type tag_id: int
fbb19900 339 """
f605e88d 340 data = {'authenticity_token': self._connection.get_token()}
a7c098e3
MM
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))
fbb19900
MM
344
345 def add(self, tag_name):
505fc964 346 """Follow new tag.
f605e88d 347 Error code 403 is accepted because pods respod with it when request
6c416e80 348 is sent to follow a tag that a user already follows.
505fc964
MM
349
350 :param tag_name: tag name
351 :type tag_name: str
6c416e80 352 :returns: int (response code)
505fc964 353 """
f605e88d
MM
354 data = {'name': tag_name,
355 'authenticity_token': self._connection.get_token(),
356 }
357 headers = {'content-type': 'application/json',
358 'x-csrf-token': self._connection.get_token(),
359 'accept': 'application/json'
360 }
f0fa9fec
MK
361
362 request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
363
6c416e80 364 if request.status_code not in [201, 403]:
505fc964 365 raise Exception('wrong error code: {0}'.format(request.status_code))
6c416e80 366 return request.status_code
2ec93347 367
7a818fdb
MM
368
369class Tag(Generic):
370 """This stream contains all posts containing a tag.
371 """
ff3d2ab4 372 def __init__(self, connection, tag):
7a818fdb
MM
373 """
374 :param connection: Connection() object
375 :type connection: diaspy.connection.Connection
376 :param tag: tag name
377 :type tag: str
378 """
379 self._connection = connection
6cd1bae0 380 self._location = 'tags/{0}.json'.format(tag)
7a818fdb 381 self.fill()