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