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