4 """This module is only imported in other diaspy modules and
5 MUST NOT import anything.
12 from diaspy
import errors
16 """This class represents an aspect.
18 Class can be initialized by passing either an id and/or name as
20 If both are missing, an exception will be raised.
22 def __init__(self
, connection
, id, name
=None):
23 self
._connection
= connection
24 self
.id, self
.name
= id, name
27 """Returns HTML returned when editing aspects via web UI.
29 start_regexp
= re
.compile('<ul +class=["\']contacts["\'] *>')
30 ajax
= self
._connection
.get('aspects/{0}/edit'.format(self
.id)).text
32 begin
= ajax
.find(start_regexp
.search(ajax
).group(0))
33 end
= ajax
.find('</ul>')
34 return ajax
[begin
:end
]
36 def _extractusernames(self
, ajax
):
37 """Extracts usernames and GUIDs from ajax returned by Diaspora*.
38 Returns list of two-tuples: (guid, diaspora_name).
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
)]
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)
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
):
51 while line
[i
].isdigit():
54 personids
[n
] = (usernames
[n
][1], id)
57 def _defineusers(self
, ajax
, personids
):
58 """Gets users contained in this aspect by getting users who have `delete` method.
60 method_regexp
= 'data-method="delete" data-person_id="{0}"'
62 for name
, id in personids
:
63 if re
.compile(method_regexp
.format(id)).search(ajax
): users
.append(name
)
66 def _getguids(self
, users_in_aspect
, usernames
):
67 """Defines users contained in this aspect.
70 for guid
, name
in usernames
:
71 if name
in users_in_aspect
: guids
.append(guid
)
75 """Returns list of GUIDs of users who are listed in this aspect.
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
)
83 def addUser(self
, user
):
84 """Add user to current aspect.
86 :param user_id: user to add to aspect
88 :returns: JSON from request
92 'person_id': user
.id(),
95 request
= self
._connection
.tokenFrom('contacts').post('aspect_memberships', data
=data
)
97 if request
.status_code
== 400:
98 raise errors
.AspectError('duplicate record, user already exists in aspect: {0}'.format(request
.status_code
))
99 elif request
.status_code
== 404:
100 raise errors
.AspectError('user not found from this pod: {0}'.format(request
.status_code
))
101 elif request
.status_code
!= 200:
102 raise errors
.AspectError('wrong status code: {0}'.format(request
.status_code
))
106 response
= request
.json()
107 except json
.decoder
.JSONDecodeError
:
108 # FIXME For some (?) reason removing users from aspects works, but
109 # adding them is a no-go and Diaspora* kicks us out with CSRF errors.
114 raise errors
.CSRFProtectionKickedIn()
118 def removeUser(self
, user
):
119 """Remove user from current aspect.
121 :param user_id: user to remove from aspect
125 for each
in user
.aspectMemberships():
127 if each
.get('aspect', {}).get('id') == self
.id:
128 membership_id
= each
.get('id')
130 if membership_id
is None:
131 raise errors
.UserIsNotMemberOfAspect(user
, self
)
133 request
= self
._connection
.delete('aspect_memberships/{0}'.format(membership_id
))
135 if request
.status_code
!= 200:
136 raise errors
.AspectError('cannot remove user from aspect: {0}'.format(request
.status_code
))
138 return request
.json()
141 class Notification():
142 """This class represents single notification.
144 _who_regexp
= re
.compile(r
'/people/[0-9a-f]+" class=\'hovercardable
')
145 _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
')
146 _aboutid_regexp = re.compile(r'/posts
/[0-9a
-f
]+')
147 _htmltag_regexp = re.compile('</?
[a
-z
]+( *[a
-z_
-]+=["\'].*?["\'])* */?
>')
149 def __init__(self, connection, data):
150 self._connection = connection
151 self.type = data['type']
152 self._data = data[self.type]
153 self.id = self._data['id']
154 self.unread = self._data['unread
']
156 def __getitem__(self, key):
157 """Returns a key from notification data.
159 return self._data[key]
162 """Returns notification note.
164 string = re.sub(self._htmltag_regexp, '', self._data['note_html
'])
165 string = string.strip().split('\n')[0]
166 while ' ' in string: string = string.replace(' ', ' ')
170 """Returns notification note with more details.
172 return '{0}
: {1}
'.format(self.when(), str(self))
175 """Returns id of post about which the notification is informing OR:
176 If the id is None it means that it's about user so
.who() is called
.
178 about = self._aboutid_regexp.search(self._data['note_html'])
179 if about is None: about = self.who()
180 else: about = int(about.group(0)[7:])
184 """Returns
list of guids of the users who caused you to get the notification
.
186 return [who[8:24] for who in self._who_regexp.findall(self._data['note_html'])]
189 """Returns UTC time
as found
in note_html
.
191 return self._data['created_at']
193 def mark(self, unread=False):
194 """Marks notification to read
/unread
.
195 Marks notification to read
if `unread`
is False.
196 Marks notification to unread
if `unread`
is True.
198 :param unread
: which state
set for notification
201 headers = {'x-csrf-token': repr(self._connection)}
202 params = {'set_unread': json.dumps(unread)}
203 self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
204 self._data['unread'] = unread
207 class Conversation():
208 """This
class represents a conversation
.
211 Remember that you need to have access to the conversation
.
213 def __init__(self, connection, id, fetch=True):
215 :param conv_id
: id of the post
and not the guid
!
217 :param connection
: connection
object used to authenticate
218 :type connection
: connection
.Connection
220 self._connection = connection
223 if fetch: self._fetch()
226 """Fetches JSON data representing conversation
.
228 request = self._connection.get('conversations/{}.json'.format(self.id))
229 if request.status_code == 200:
230 self._data = request.json()['conversation']
232 raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
234 def answer(self, text):
235 """Answer that conversation
237 :param text
: text to answer
.
240 data = {'message[text]': text,
242 'authenticity_token': repr(self._connection)}
244 request = self._connection.post('conversations/{}/messages'.format(self.id),
246 headers={'accept': 'application/json'})
247 if request.status_code != 200:
248 raise errors.ConversationError('{0}: Answer could not be posted.'
249 .format(request.status_code))
250 return request.json()
253 """Delete this conversation
.
254 Has to be implemented
.
256 data = {'authenticity_token': repr(self._connection)}
258 request = self._connection.delete('conversations/{0}/visibility/'
261 headers={'accept': 'application/json'})
263 if request.status_code != 404:
264 raise errors.ConversationError('{0}: Conversation could not be deleted.'
265 .format(request.status_code))
267 def get_subject(self):
268 """Returns the subject of this conversation
270 return self._data['subject']
274 """Represents comment on post
.
276 Does
not require
Connection() object. Note that you should
not manually
277 create `
Comment()` objects
-- they are designed to be created automatically
280 def __init__(self, data):
284 """Returns comment
's text.
286 return self._data['text
']
289 """Returns comments text and author.
290 Format: AUTHOR (AUTHOR'S GUID
): COMMENT
292 return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
295 """Returns time when the comment had been created
.
297 return self._data['created_at']
299 def author(self, key='name'):
300 """Returns author of the comment
.
302 return self._data['author'][key]
306 """This
class represents a post
.
309 Remember that you need to have access to the post
.
311 def __init__(self, connection, id=0, guid='', fetch=True, comments=True):
313 :param
id: id of the
post (GUID
is recommended
)
315 :param guid
: GUID of the post
317 :param connection
: connection
object used to authenticate
318 :type connection
: connection
.Connection
319 :param fetch
: defines whether to fetch post
's data or not
321 :param comments: defines whether to fetch post's comments
or not (if True also data will be fetched
)
324 if not (guid or id): raise TypeError('neither guid nor id was provided')
325 self._connection = connection
330 if fetch: self._fetchdata()
332 if not self._data: self._fetchdata()
333 self._fetchcomments()
336 """Returns string containing more information then
str().
338 return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
341 """Returns text of a post
.
343 return self._data['text']
345 def __getitem__(self, key):
346 return self._data[key]
349 """Returns dictionary of posts data
.
353 def _fetchdata(self):
354 """This function retrieves data of the post
.
356 :returns
: guid of post whose data was fetched
358 if self.id: id = self.id
359 if self.guid: id = self.guid
360 request = self._connection.get('posts/{0}.json'.format(id))
361 if request.status_code != 200:
362 raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
364 self._data = request.json()
367 def _fetchcomments(self):
368 """Retreives comments
for this post
.
369 Retrieving comments via GUID will result
in 404 error
.
370 DIASPORA
* does
not supply comments through
/posts
/:guid
/ endpoint
.
372 id = self._data['id']
373 if self['interactions']['comments_count']:
374 request = self._connection.get('posts/{0}/comments.json'.format(id))
375 if request.status_code != 200:
376 raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
378 self.comments = [Comment(c) for c in request.json()]
381 """Updates post data
.
384 self._fetchcomments()
387 """This function likes a post
.
388 It abstracts the
'Like' functionality
.
390 :returns
: dict -- json formatted like
object.
392 data = {'authenticity_token': repr(self._connection)}
394 request = self._connection.post('posts/{0}/likes'.format(self.id),
396 headers={'accept': 'application/json'})
398 if request.status_code != 201:
399 raise errors.PostError('{0}: Post could not be liked.'
400 .format(request.status_code))
401 return request.json()
404 """This function reshares a post
406 data = {'root_guid': self._data['guid'],
407 'authenticity_token': repr(self._connection)}
409 request = self._connection.post('reshares',
411 headers={'accept': 'application/json'})
412 if request.status_code != 201:
413 raise Exception('{0}: Post could not be reshared'.format(request.status_code))
414 return request.json()
416 def comment(self, text):
417 """This function comments on a post
419 :param text
: text to comment
.
422 data = {'text': text,
423 'authenticity_token': repr(self._connection)}
424 request = self._connection.post('posts/{0}/comments'.format(self.id),
426 headers={'accept': 'application/json'})
428 if request.status_code != 201:
429 raise Exception('{0}: Comment could not be posted.'
430 .format(request.status_code))
431 return request.json()
434 """ This function deletes this post
436 data = {'authenticity_token': repr(self._connection)}
437 request = self._connection.delete('posts/{0}'.format(self.id),
439 headers={'accept': 'application/json'})
440 if request.status_code != 204:
441 raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
443 def delete_comment(self, comment_id):
444 """This function removes a comment
from a post
446 :param comment_id
: id of the comment to remove
.
447 :type comment_id
: str
449 data = {'authenticity_token': repr(self._connection)}
450 request = self._connection.delete('posts/{0}/comments/{1}'
451 .format(self.id, comment_id),
453 headers={'accept': 'application/json'})
455 if request.status_code != 204:
456 raise errors.PostError('{0}: Comment could not be deleted'
457 .format(request.status_code))
459 def delete_like(self):
460 """This function removes a like
from a post
462 data = {'authenticity_token': repr(self._connection)}
463 url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
464 request = self._connection.delete(url, data=data)
465 if request.status_code != 204:
466 raise errors.PostError('{0}: Like could not be removed.'
467 .format(request.status_code))
469 def author(self, key='name'):
470 """Returns author of the post
.
471 :param key
: all keys available
in data
['author']
473 return self._data['author'][key]