More changes in token getting procedure, now you can specify whether
[diaspy.git] / diaspy / streams.py
CommitLineData
1232dac5 1import json
33b34938 2import time
d589deff 3from diaspy.models import Post, Aspect
1232dac5 4
505fc964
MM
5"""Docstrings for this module are taken from:
6https://gist.github.com/MrZYX/01c93096c30dc44caf71
7
8Documentation for D* JSON API taken from:
9http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
10"""
1232dac5 11
f605e88d 12
7a818fdb
MM
13class Generic():
14 """Object representing generic stream.
1232dac5 15 """
27a28aaf
MM
16 _location = 'stream.json'
17 _stream = []
33b34938
MM
18 # since epoch
19 max_time = int(time.mktime(time.gmtime()))
27a28aaf 20
505fc964 21 def __init__(self, connection, location=''):
1232dac5
MM
22 """
23 :param connection: Connection() object
505fc964 24 :type connection: diaspy.connection.Connection
27a28aaf 25 :param location: location of json (optional)
505fc964 26 :type location: str
1232dac5
MM
27 """
28 self._connection = connection
505fc964 29 if location: self._location = location
1232dac5
MM
30 self.fill()
31
32 def __contains__(self, post):
33 """Returns True if stream contains given post.
34 """
1232dac5
MM
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
505fc964
MM
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
1232dac5
MM
80 def update(self):
81 """Updates stream.
82 """
505fc964
MM
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
1232dac5
MM
93
94 def fill(self):
95 """Fills the stream with posts.
96 """
97 self._stream = self._obtain()
98
178faa46 99 def more(self, max_time=0):
33b34938
MM
100 """Tries to download more (older ones) Posts from Stream.
101 """
178faa46
MM
102 if not max_time: max_time = self.max_time - 3000000
103 self.max_time = max_time
27f09973 104 params = {'max_time': self.max_time}
178faa46 105 request = self._connection.get(self._location, params=params)
33b34938
MM
106 if request.status_code != 200:
107 raise Exception('wrong status code: {0}'.format(request.status_code))
108
1232dac5 109
beaa09fb
MM
110class 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
1232dac5 123class Stream(Generic):
f605e88d
MM
124 """The main stream containing the combined posts of the
125 followed users and tags and the community spotlights posts
505fc964 126 if the user enabled those.
1232dac5 127 """
27a28aaf 128 location = 'stream.json'
505fc964 129
a98c6792
MM
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.
1232dac5
MM
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
a98c6792
MM
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
1232dac5
MM
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}
a98c6792 148 if photo: data['photos'] = self._photoupload(photo)
1232dac5 149 if photos: data['photos'] = photos
a98c6792 150
1232dac5
MM
151 request = self._connection.post('status_messages',
152 data=json.dumps(data),
153 headers={'content-type': 'application/json',
154 'accept': 'application/json',
59ad210c 155 'x-csrf-token': self._connection.get_token()})
1232dac5 156 if request.status_code != 201:
a98c6792 157 raise Exception('{0}: Post could not be posted.'.format(request.status_code))
1232dac5
MM
158
159 post = Post(str(request.json()['id']), self._connection)
160 return post
161
66c3bb76
MM
162 def _photoupload(self, filename):
163 """Uploads picture to the pod.
1232dac5 164
66c3bb76 165 :param filename: path to picture file
1232dac5 166 :type filename: str
66c3bb76
MM
167
168 :returns: id of the photo being uploaded
1232dac5 169 """
38fabb63
MM
170 data = open(filename, 'rb')
171 image = data.read()
172 data.close()
173
1232dac5
MM
174 params = {}
175 params['photo[pending]'] = 'true'
176 params['set_profile_image'] = ''
177 params['qqfile'] = filename
38fabb63 178 aspects = self._connection.getUserInfo()['aspects']
1232dac5 179 for i, aspect in enumerate(aspects):
38fabb63 180 params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
1232dac5
MM
181
182 headers = {'content-type': 'application/octet-stream',
59ad210c 183 'x-csrf-token': self._connection.get_token(),
1232dac5 184 'x-file-name': filename}
38fabb63
MM
185
186 request = self._connection.post('photos', data=image, params=params, headers=headers)
187 if request.status_code != 200:
66c3bb76
MM
188 raise Exception('wrong error code: {0}'.format(request.status_code))
189 return request.json()['data']['photo']['id']
190
1232dac5 191
278febce 192class Activity(Stream):
1232dac5
MM
193 """Stream representing user's activity.
194 """
27a28aaf 195 _location = 'activity.json'
505fc964
MM
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:
27a28aaf 219 raise TypeError('this method accepts str or Post types: {0} given')
505fc964
MM
220 self.fill()
221
222
223class Aspects(Generic):
f605e88d
MM
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.
505fc964
MM
229 An example call would be `aspects.json?aspect_ids=23,5,42`
230 """
27a28aaf 231 _location = 'aspects.json'
27a28aaf 232
278febce 233 def getAspectID(self, aspect_name):
63cc182d
MM
234 """Returns id of an aspect of given name.
235 Returns -1 if aspect is not found.
236
278febce
MM
237 :param aspect_name: aspect name (must be spelled exactly as when created)
238 :type aspect_name: str
63cc182d
MM
239 :returns: int
240 """
241 id = -1
33b34938 242 aspects = self._connection.getUserInfo()['aspects']
278febce
MM
243 for aspect in aspects:
244 if aspect['name'] == aspect_name: id = aspect['id']
63cc182d
MM
245 return id
246
27a28aaf
MM
247 def filterByIDs(self, ids):
248 self._location += '?{0}'.format(','.join(ids))
249 self.fill()
250
505fc964
MM
251 def add(self, aspect_name, visible=0):
252 """This function adds a new aspect.
278febce 253 Status code 422 is accepted because it is returned by D* when
27a28aaf
MM
254 you try to add aspect already present on your aspect list.
255
d589deff 256 :returns: Aspect() object of just created aspect
505fc964 257 """
59ad210c 258 data = {'authenticity_token': self._connection.get_token(),
505fc964
MM
259 'aspect[name]': aspect_name,
260 'aspect[contacts_visible]': visible}
261
27a28aaf
MM
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
278febce 266 id = self.getAspectID(aspect_name)
d589deff 267 return Aspect(self._connection, id)
505fc964 268
278febce 269 def remove(self, aspect_id=-1, name=''):
fbb19900 270 """This method removes an aspect.
278febce
MM
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
27a28aaf
MM
275 go nuts it will remove the aspect anyway.
276
277 :param aspect_id: id fo aspect to remove
278 :type aspect_id: int
1467ec15
MM
279 :param name: name of aspect to remove
280 :type name: str
fbb19900 281 """
278febce 282 if aspect_id == -1 and name: aspect_id = self.getAspectID(name)
27f09973 283 data = {'_method': 'delete',
dd0a4d9f
MM
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))
fbb19900 288
505fc964
MM
289
290class Commented(Generic):
f605e88d 291 """This stream contains all posts
505fc964
MM
292 the user has made a comment on.
293 """
27a28aaf 294 _location = 'commented.json'
505fc964
MM
295
296
297class Liked(Generic):
298 """This stream contains all posts the user liked.
299 """
27a28aaf 300 _location = 'liked.json'
505fc964
MM
301
302
303class Mentions(Generic):
f605e88d 304 """This stream contains all posts
505fc964
MM
305 the user is mentioned in.
306 """
27a28aaf 307 _location = 'mentions.json'
505fc964
MM
308
309
310class FollowedTags(Generic):
f605e88d 311 """This stream contains all posts
6c416e80 312 containing tags the user is following.
505fc964 313 """
27a28aaf 314 _location = 'followed_tags.json'
505fc964 315
a7c098e3 316 def remove(self, tag_id):
fbb19900
MM
317 """Stop following a tag.
318
a7c098e3
MM
319 :param tag_id: tag id
320 :type tag_id: int
fbb19900 321 """
f605e88d 322 data = {'authenticity_token': self._connection.get_token()}
a7c098e3
MM
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))
fbb19900
MM
326
327 def add(self, tag_name):
505fc964 328 """Follow new tag.
f605e88d 329 Error code 403 is accepted because pods respod with it when request
6c416e80 330 is sent to follow a tag that a user already follows.
505fc964
MM
331
332 :param tag_name: tag name
333 :type tag_name: str
6c416e80 334 :returns: int (response code)
505fc964 335 """
f605e88d
MM
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 }
f0fa9fec
MK
343
344 request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
345
6c416e80 346 if request.status_code not in [201, 403]:
505fc964 347 raise Exception('wrong error code: {0}'.format(request.status_code))
6c416e80 348 return request.status_code
7a818fdb
MM
349
350
351class Tag(Generic):
352 """This stream contains all posts containing a tag.
353 """
ff3d2ab4 354 def __init__(self, connection, tag):
7a818fdb
MM
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()