Merge branch 'people' into streams
[diaspy.git] / diaspy / streams.py
CommitLineData
1232dac5
MM
1import json
2from diaspy.models import Post
3
505fc964
MM
4"""Docstrings for this module are taken from:
5https://gist.github.com/MrZYX/01c93096c30dc44caf71
6
7Documentation for D* JSON API taken from:
8http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
9"""
1232dac5
MM
10
11class 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
119class 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 132class 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
213class 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
245class 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
277class 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
285class Liked(Generic):
286 """This stream contains all posts the user liked.
287 """
288 def _setlocation(self):
289 self._location = 'liked.json'
290
291
292class 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
300class 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