Commit | Line | Data |
---|---|---|
1232dac5 | 1 | import json |
33b34938 | 2 | import time |
d589deff | 3 | from diaspy.models import Post, Aspect |
1232dac5 | 4 | |
505fc964 MM |
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 | """ | |
1232dac5 | 11 | |
f605e88d | 12 | |
7a818fdb MM |
13 | class 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 |
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 | ||
1232dac5 | 123 | class 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 | 192 | class 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 | ||
223 | class 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 | |
290 | class 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 | ||
297 | class Liked(Generic): | |
298 | """This stream contains all posts the user liked. | |
299 | """ | |
27a28aaf | 300 | _location = 'liked.json' |
505fc964 MM |
301 | |
302 | ||
303 | class 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 | ||
310 | class 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 | ||
351 | class 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() |