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