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