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