Access method for getting id of a user
[diaspy.git] / diaspy / models.py
1 #!/usr/bin/env python3
2
3
4 """This module is only imported in other diaspy modules and
5 MUST NOT import anything.
6 """
7
8
9 import json
10 import re
11
12 from diaspy import errors
13
14
15 class Aspect():
16 """This class represents an aspect.
17
18 Class can be initialized by passing either an id and/or name as
19 parameters.
20 If both are missing, an exception will be raised.
21 """
22 def __init__(self, connection, id, name=None):
23 self._connection = connection
24 self.id, self.name = id, name
25
26 def _getajax(self):
27 """Returns HTML returned when editing aspects via web UI.
28 """
29 start_regexp = re.compile('<ul +class=["\']contacts["\'] *>')
30 ajax = self._connection.get('aspects/{0}/edit'.format(self.id)).text
31
32 begin = ajax.find(start_regexp.search(ajax).group(0))
33 end = ajax.find('</ul>')
34 return ajax[begin:end]
35
36 def _extractusernames(self, ajax):
37 """Extracts usernames and GUIDs from ajax returned by Diaspora*.
38 Returns list of two-tuples: (guid, diaspora_name).
39 """
40 userline_regexp = re.compile('<a href=["\']/people/[a-z0-9]{16,16}["\']>[\w()*@. -]+</a>')
41 return [(line[17:33], re.escape(line[35:-4])) for line in userline_regexp.findall(ajax)]
42
43 def _extractpersonids(self, ajax, usernames):
44 """Extracts `person_id`s and usernames from ajax and list of usernames.
45 Returns list of two-tuples: (username, id)
46 """
47 personid_regexp = 'alt=["\']{0}["\'] class=["\']avatar["\'] data-person_id=["\'][0-9]+["\']'
48 personids = [re.compile(personid_regexp.format(name)).search(ajax).group(0) for guid, name in usernames]
49 for n, line in enumerate(personids):
50 i, id = -2, ''
51 while line[i].isdigit():
52 id = line[i] + id
53 i -= 1
54 personids[n] = (usernames[n][1], id)
55 return personids
56
57 def _defineusers(self, ajax, personids):
58 """Gets users contained in this aspect by getting users who have `delete` method.
59 """
60 method_regexp = 'data-method="delete" data-person_id="{0}"'
61 users = []
62 for name, id in personids:
63 if re.compile(method_regexp.format(id)).search(ajax): users.append(name)
64 return users
65
66 def _getguids(self, users_in_aspect, usernames):
67 """Defines users contained in this aspect.
68 """
69 guids = []
70 for guid, name in usernames:
71 if name in users_in_aspect: guids.append(guid)
72 return guids
73
74 def getUsers(self):
75 """Returns list of GUIDs of users who are listed in this aspect.
76 """
77 ajax = self._getajax()
78 usernames = self._extractusernames(ajax)
79 personids = self._extractpersonids(ajax, usernames)
80 users_in_aspect = self._defineusers(ajax, personids)
81 return self._getguids(users_in_aspect, usernames)
82
83 def addUser(self, user_id):
84 """Add user to current aspect.
85
86 :param user_id: user to add to aspect
87 :type user_id: int
88 :returns: JSON from request
89 """
90 data = {'authenticity_token': repr(self._connection),
91 'aspect_id': self.id,
92 'person_id': user_id}
93
94 request = self._connection.post('aspect_memberships.json', data=data)
95
96 if request.status_code == 400:
97 raise errors.AspectError('duplicate record, user already exists in aspect: {0}'.format(request.status_code))
98 elif request.status_code == 404:
99 raise errors.AspectError('user not found from this pod: {0}'.format(request.status_code))
100 elif request.status_code != 200:
101 raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
102 return request.json()
103
104 def removeUser(self, user):
105 """Remove user from current aspect.
106
107 :param user_id: user to remove from aspect
108 :type user: int
109 """
110 membership_id = None
111 for each in user.aspectMemberships():
112 print(self.id, each)
113 if each.get('aspect', {}).get('id') == self.id:
114 membership_id = each.get('id')
115
116 if membership_id is None:
117 raise errors.UserIsNotMemberOfAspect(user, self)
118
119 request = self._connection.delete('aspect_memberships/{0}'.format(membership_id))
120
121 if request.status_code != 200:
122 raise errors.AspectError('cannot remove user from aspect: {0}'.format(request.status_code))
123
124 return request.json()
125
126
127 class Notification():
128 """This class represents single notification.
129 """
130 _who_regexp = re.compile(r'/people/[0-9a-f]+" class=\'hovercardable')
131 _when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
132 _aboutid_regexp = re.compile(r'/posts/[0-9a-f]+')
133 _htmltag_regexp = re.compile('</?[a-z]+( *[a-z_-]+=["\'].*?["\'])* */?>')
134
135 def __init__(self, connection, data):
136 self._connection = connection
137 self.type = list(data.keys())[0]
138 self._data = data[self.type]
139 self.id = self._data['id']
140 self.unread = self._data['unread']
141
142 def __getitem__(self, key):
143 """Returns a key from notification data.
144 """
145 return self._data[key]
146
147 def __str__(self):
148 """Returns notification note.
149 """
150 string = re.sub(self._htmltag_regexp, '', self._data['note_html'])
151 string = string.strip().split('\n')[0]
152 while ' ' in string: string = string.replace(' ', ' ')
153 return string
154
155 def __repr__(self):
156 """Returns notification note with more details.
157 """
158 return '{0}: {1}'.format(self.when(), str(self))
159
160 def about(self):
161 """Returns id of post about which the notification is informing OR:
162 If the id is None it means that it's about user so .who() is called.
163 """
164 about = self._aboutid_regexp.search(self._data['note_html'])
165 if about is None: about = self.who()
166 else: about = int(about.group(0)[7:])
167 return about
168
169 def who(self):
170 """Returns list of guids of the users who caused you to get the notification.
171 """
172 return [who[8:24] for who in self._who_regexp.findall(self._data['note_html'])]
173
174 def when(self):
175 """Returns UTC time as found in note_html.
176 """
177 return self._data['created_at']
178
179 def mark(self, unread=False):
180 """Marks notification to read/unread.
181 Marks notification to read if `unread` is False.
182 Marks notification to unread if `unread` is True.
183
184 :param unread: which state set for notification
185 :type unread: bool
186 """
187 headers = {'x-csrf-token': repr(self._connection)}
188 params = {'set_unread': json.dumps(unread)}
189 self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
190 self._data['unread'] = unread
191
192
193 class Conversation():
194 """This class represents a conversation.
195
196 .. note::
197 Remember that you need to have access to the conversation.
198 """
199 def __init__(self, connection, id, fetch=True):
200 """
201 :param conv_id: id of the post and not the guid!
202 :type conv_id: str
203 :param connection: connection object used to authenticate
204 :type connection: connection.Connection
205 """
206 self._connection = connection
207 self.id = id
208 self._data = {}
209 if fetch: self._fetch()
210
211 def _fetch(self):
212 """Fetches JSON data representing conversation.
213 """
214 request = self._connection.get('conversations/{}.json'.format(self.id))
215 if request.status_code == 200:
216 self._data = request.json()['conversation']
217 else:
218 raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
219
220 def answer(self, text):
221 """Answer that conversation
222
223 :param text: text to answer.
224 :type text: str
225 """
226 data = {'message[text]': text,
227 'utf8': '&#x2713;',
228 'authenticity_token': repr(self._connection)}
229
230 request = self._connection.post('conversations/{}/messages'.format(self.id),
231 data=data,
232 headers={'accept': 'application/json'})
233 if request.status_code != 200:
234 raise errors.ConversationError('{0}: Answer could not be posted.'
235 .format(request.status_code))
236 return request.json()
237
238 def delete(self):
239 """Delete this conversation.
240 Has to be implemented.
241 """
242 data = {'authenticity_token': repr(self._connection)}
243
244 request = self._connection.delete('conversations/{0}/visibility/'
245 .format(self.id),
246 data=data,
247 headers={'accept': 'application/json'})
248
249 if request.status_code != 404:
250 raise errors.ConversationError('{0}: Conversation could not be deleted.'
251 .format(request.status_code))
252
253 def get_subject(self):
254 """Returns the subject of this conversation
255 """
256 return self._data['subject']
257
258
259 class Comment():
260 """Represents comment on post.
261
262 Does not require Connection() object. Note that you should not manually
263 create `Comment()` objects -- they are designed to be created automatically
264 by `Post()` objects.
265 """
266 def __init__(self, data):
267 self._data = data
268
269 def __str__(self):
270 """Returns comment's text.
271 """
272 return self._data['text']
273
274 def __repr__(self):
275 """Returns comments text and author.
276 Format: AUTHOR (AUTHOR'S GUID): COMMENT
277 """
278 return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
279
280 def when(self):
281 """Returns time when the comment had been created.
282 """
283 return self._data['created_at']
284
285 def author(self, key='name'):
286 """Returns author of the comment.
287 """
288 return self._data['author'][key]
289
290
291 class Post():
292 """This class represents a post.
293
294 .. note::
295 Remember that you need to have access to the post.
296 """
297 def __init__(self, connection, id=0, guid='', fetch=True, comments=True):
298 """
299 :param id: id of the post (GUID is recommended)
300 :type id: int
301 :param guid: GUID of the post
302 :type guid: str
303 :param connection: connection object used to authenticate
304 :type connection: connection.Connection
305 :param fetch: defines whether to fetch post's data or not
306 :type fetch: bool
307 :param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
308 :type comments: bool
309 """
310 if not (guid or id): raise TypeError('neither guid nor id was provided')
311 self._connection = connection
312 self.id = id
313 self.guid = guid
314 self._data = {}
315 self.comments = []
316 if fetch: self._fetchdata()
317 if comments:
318 if not self._data: self._fetchdata()
319 self._fetchcomments()
320
321 def __repr__(self):
322 """Returns string containing more information then str().
323 """
324 return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
325
326 def __str__(self):
327 """Returns text of a post.
328 """
329 return self._data['text']
330
331 def __getitem__(self, key):
332 return self._data[key]
333
334 def __dict__(self):
335 """Returns dictionary of posts data.
336 """
337 return self._data
338
339 def _fetchdata(self):
340 """This function retrieves data of the post.
341
342 :returns: guid of post whose data was fetched
343 """
344 if self.id: id = self.id
345 if self.guid: id = self.guid
346 request = self._connection.get('posts/{0}.json'.format(id))
347 if request.status_code != 200:
348 raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
349 else:
350 self._data = request.json()
351 return self['guid']
352
353 def _fetchcomments(self):
354 """Retreives comments for this post.
355 Retrieving comments via GUID will result in 404 error.
356 DIASPORA* does not supply comments through /posts/:guid/ endpoint.
357 """
358 id = self._data['id']
359 if self['interactions']['comments_count']:
360 request = self._connection.get('posts/{0}/comments.json'.format(id))
361 if request.status_code != 200:
362 raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
363 else:
364 self.comments = [Comment(c) for c in request.json()]
365
366 def update(self):
367 """Updates post data.
368 """
369 self._fetchdata()
370 self._fetchcomments()
371
372 def like(self):
373 """This function likes a post.
374 It abstracts the 'Like' functionality.
375
376 :returns: dict -- json formatted like object.
377 """
378 data = {'authenticity_token': repr(self._connection)}
379
380 request = self._connection.post('posts/{0}/likes'.format(self.id),
381 data=data,
382 headers={'accept': 'application/json'})
383
384 if request.status_code != 201:
385 raise errors.PostError('{0}: Post could not be liked.'
386 .format(request.status_code))
387 return request.json()
388
389 def reshare(self):
390 """This function reshares a post
391 """
392 data = {'root_guid': self._data['guid'],
393 'authenticity_token': repr(self._connection)}
394
395 request = self._connection.post('reshares',
396 data=data,
397 headers={'accept': 'application/json'})
398 if request.status_code != 201:
399 raise Exception('{0}: Post could not be reshared'.format(request.status_code))
400 return request.json()
401
402 def comment(self, text):
403 """This function comments on a post
404
405 :param text: text to comment.
406 :type text: str
407 """
408 data = {'text': text,
409 'authenticity_token': repr(self._connection)}
410 request = self._connection.post('posts/{0}/comments'.format(self.id),
411 data=data,
412 headers={'accept': 'application/json'})
413
414 if request.status_code != 201:
415 raise Exception('{0}: Comment could not be posted.'
416 .format(request.status_code))
417 return request.json()
418
419 def delete(self):
420 """ This function deletes this post
421 """
422 data = {'authenticity_token': repr(self._connection)}
423 request = self._connection.delete('posts/{0}'.format(self.id),
424 data=data,
425 headers={'accept': 'application/json'})
426 if request.status_code != 204:
427 raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
428
429 def delete_comment(self, comment_id):
430 """This function removes a comment from a post
431
432 :param comment_id: id of the comment to remove.
433 :type comment_id: str
434 """
435 data = {'authenticity_token': repr(self._connection)}
436 request = self._connection.delete('posts/{0}/comments/{1}'
437 .format(self.id, comment_id),
438 data=data,
439 headers={'accept': 'application/json'})
440
441 if request.status_code != 204:
442 raise errors.PostError('{0}: Comment could not be deleted'
443 .format(request.status_code))
444
445 def delete_like(self):
446 """This function removes a like from a post
447 """
448 data = {'authenticity_token': repr(self._connection)}
449 url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
450 request = self._connection.delete(url, data=data)
451 if request.status_code != 204:
452 raise errors.PostError('{0}: Like could not be removed.'
453 .format(request.status_code))
454
455 def author(self, key='name'):
456 """Returns author of the post.
457 :param key: all keys available in data['author']
458 """
459 return self._data['author'][key]