Fixed bug aspect deletion, initial development for contacts
[diaspy.git] / diaspy / streams.py
CommitLineData
1232dac5 1import json
27a28aaf 2import re
33b34938 3import time
1232dac5
MM
4from diaspy.models import Post
5
505fc964
MM
6"""Docstrings for this module are taken from:
7https://gist.github.com/MrZYX/01c93096c30dc44caf71
8
9Documentation for D* JSON API taken from:
10http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
11"""
1232dac5 12
f605e88d 13
1232dac5 14class 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
113class 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 126class 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 195class 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
226class 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
293class 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
300class Liked(Generic):
301 """This stream contains all posts the user liked.
302 """
27a28aaf 303 _location = 'liked.json'
505fc964
MM
304
305
306class 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
313class 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