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=None, name
=None):
23 self
._connection
= connection
24 self
.id, self
.name
= id, name
26 self
.name
= self
._findname
()
28 self
.id = self
._findid
()
29 elif not id and not name
:
30 raise Exception('Aspect must be initialized with either an id or name')
33 """Finds name for aspect.
36 aspects
= self
._connection
.getUserInfo()['aspects']
38 if a
['id'] == self
.id:
44 """Finds id for aspect.
47 aspects
= self
._connection
.getUserInfo()['aspects']
49 if a
['name'] == self
.name
:
55 """Returns HTML returned when editing aspects via web UI.
57 start_regexp
= re
.compile('<ul +class=["\']contacts["\'] *>')
58 ajax
= self
._connection
.get('aspects/{0}/edit'.format(self
.id)).text
60 begin
= ajax
.find(start_regexp
.search(ajax
).group(0))
61 end
= ajax
.find('</ul>')
62 return ajax
[begin
:end
]
64 def _extractusernames(self
, ajax
):
65 """Extracts usernames and GUIDs from ajax returned by Diaspora*.
66 Returns list of two-tuples: (guid, diaspora_name).
68 userline_regexp
= re
.compile('<a href=["\']/people/[a-z0-9]{16,16}["\']>[\w()*@. -]+</a>')
69 return [(line
[17:33], re
.escape(line
[35:-4])) for line
in userline_regexp
.findall(ajax
)]
71 def _extractpersonids(self
, ajax
, usernames
):
72 """Extracts `person_id`s and usernames from ajax and list of usernames.
73 Returns list of two-tuples: (username, id)
75 personid_regexp
= 'alt=["\']{0}["\'] class=["\']avatar["\'] data-person_id=["\'][0-9]+["\']'
76 personids
= [re
.compile(personid_regexp
.format(name
)).search(ajax
).group(0) for guid
, name
in usernames
]
77 for n
, line
in enumerate(personids
):
79 while line
[i
].isdigit():
82 personids
[n
] = (usernames
[n
][1], id)
85 def _defineusers(self
, ajax
, personids
):
86 """Gets users contained in this aspect by getting users who have `delete` method.
88 method_regexp
= 'data-method="delete" data-person_id="{0}"'
90 for name
, id in personids
:
91 if re
.compile(method_regexp
.format(id)).search(ajax
): users
.append(name
)
94 def _getguids(self
, users_in_aspect
, usernames
):
95 """Defines users contained in this aspect.
98 for guid
, name
in usernames
:
99 if name
in users_in_aspect
: guids
.append(guid
)
103 """Returns list of GUIDs of users who are listed in this aspect.
105 ajax
= self
._getajax
()
106 usernames
= self
._extractusernames
(ajax
)
107 personids
= self
._extractpersonids
(ajax
, usernames
)
108 users_in_aspect
= self
._defineusers
(ajax
, personids
)
109 return self
._getguids
(users_in_aspect
, usernames
)
111 def addUser(self
, user_id
):
112 """Add user to current aspect.
114 :param user_id: user to add to aspect
116 :returns: JSON from request
118 data
= {'authenticity_token': repr(self
._connection
),
119 'aspect_id': self
.id,
120 'person_id': user_id
}
122 request
= self
._connection
.post('aspect_memberships.json', data
=data
)
124 if request
.status_code
== 400:
125 raise errors
.AspectError('duplicate record, user already exists in aspect: {0}'.format(request
.status_code
))
126 elif request
.status_code
== 404:
127 raise errors
.AspectError('user not found from this pod: {0}'.format(request
.status_code
))
128 elif request
.status_code
!= 200:
129 raise errors
.AspectError('wrong status code: {0}'.format(request
.status_code
))
130 return request
.json()
132 def removeUser(self
, user_id
):
133 """Remove user from current aspect.
135 :param user_id: user to remove from aspect
138 data
= {'authenticity_token': repr(self
._connection
),
139 'aspect_id': self
.id,
140 'person_id': user_id
}
141 request
= self
.connection
.delete('aspect_memberships/{0}.json'.format(self
.id), data
=data
)
143 if request
.status_code
!= 200:
144 raise errors
.AspectError('cannot remove user from aspect: {0}'.format(request
.status_code
))
145 return request
.json()
148 class Notification():
149 """This class represents single notification.
151 _who_regexp
= re
.compile(r
'/people/[0-9a-f]+" class=\'hovercardable
')
152 _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
')
153 _aboutid_regexp = re.compile(r'/posts
/[0-9a
-f
]+')
154 _htmltag_regexp = re.compile('</?
[a
-z
]+( *[a
-z_
-]+=["\'].*?["\'])* */?
>')
156 def __init__(self, connection, data):
157 self._connection = connection
158 self.type = list(data.keys())[0]
159 self.data = data[self.type]
160 self.id = self.data['id']
161 self.unread = self.data['unread
']
163 def __getitem__(self, key):
164 """Returns a key from notification data.
166 return self.data[key]
169 """Returns notification note.
171 string = re.sub(self._htmltag_regexp, '', self.data['note_html
'])
172 string = string.strip().split('\n')[0]
173 while ' ' in string: string = string.replace(' ', ' ')
177 """Returns notification note with more details.
179 return '{0}
: {1}
'.format(self.when(), str(self))
182 """Returns id of post about which the notification is informing OR:
183 If the id is None it means that it's about user so
.who() is called
.
185 about = self._aboutid_regexp.search(self.data['note_html'])
186 if about is None: about = self.who()
187 else: about = int(about.group(0)[7:])
191 """Returns
list of guids of the users who caused you to get the notification
.
193 return [who[8:24] for who in self._who_regexp.findall(self.data['note_html'])]
196 """Returns UTC time
as found
in note_html
.
198 return self._when_regexp.search(self.data['note_html']).group(0)
200 def mark(self, unread=False):
201 """Marks notification to read
/unread
.
202 Marks notification to read
if `unread`
is False.
203 Marks notification to unread
if `unread`
is True.
205 :param unread
: which state
set for notification
208 headers = {'x-csrf-token': repr(self._connection)}
209 params = {'set_unread': json.dumps(unread)}
210 self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
211 self.data['unread'] = unread
214 class Conversation():
215 """This
class represents a conversation
.
218 Remember that you need to have access to the conversation
.
220 def __init__(self, connection, id, fetch=True):
222 :param conv_id
: id of the post
and not the guid
!
224 :param connection
: connection
object used to authenticate
225 :type connection
: connection
.Connection
227 self._connection = connection
230 if fetch: self._fetch()
233 """Fetches JSON data representing conversation
.
235 request = self._connection.get('conversations/{}.json'.format(self.id))
236 if request.status_code == 200:
237 self.data = request.json()['conversation']
239 raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
241 def answer(self, text):
242 """Answer that conversation
244 :param text
: text to answer
.
247 data = {'message[text]': text,
249 'authenticity_token': repr(self._connection)}
251 request = self._connection.post('conversations/{}/messages'.format(self.id),
253 headers={'accept': 'application/json'})
254 if request.status_code != 200:
255 raise errors.ConversationError('{0}: Answer could not be posted.'
256 .format(request.status_code))
257 return request.json()
260 """Delete this conversation
.
261 Has to be implemented
.
263 data = {'authenticity_token': repr(self._connection)}
265 request = self._connection.delete('conversations/{0}/visibility/'
268 headers={'accept': 'application/json'})
270 if request.status_code != 404:
271 raise errors.ConversationError('{0}: Conversation could not be deleted.'
272 .format(request.status_code))
274 def get_subject(self):
275 """Returns the subject of this conversation
277 return self.data['subject']
281 """Represents comment on post
.
283 Does
not require
Connection() object. Note that you should
not manually
284 create `
Comment()` objects
-- they are designed to be created automatically
287 def __init__(self, data):
291 """Returns comment
's text.
293 return self.data['text
']
296 """Returns comments text and author.
297 Format: AUTHOR (AUTHOR'S GUID
): COMMENT
299 return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
302 """Returns time when the comment had been created
.
304 return self.data['created_at']
306 def author(self, key='name'):
307 """Returns author of the comment
.
309 return self.data['author'][key]
313 """This
class represents a post
.
316 Remember that you need to have access to the post
.
318 def __init__(self, connection, id=0, guid='', fetch=True, comments=True):
320 :param
id: id of the
post (GUID
is recommended
)
322 :param guid
: GUID of the post
324 :param connection
: connection
object used to authenticate
325 :type connection
: connection
.Connection
326 :param fetch
: defines whether to fetch post
's data or not
328 :param comments: defines whether to fetch post's comments
or not (if True also data will be fetched
)
331 if not (guid or id): raise TypeError('guid and/or id missing')
332 self._connection = connection
337 if fetch: self._fetchdata()
339 if not self.data: self._fetchdata()
340 self._fetchcomments()
343 """Returns string containing more information then
str().
345 return '{0} ({1}): {2}'.format(self.data['author']['name'], self.data['author']['guid'], self.data['text'])
348 """Returns text of a post
.
350 return self.data['text']
352 def __getitem__(self, key):
353 return self.data[key]
356 """Returns dictionary of posts data
.
360 def _fetchdata(self):
361 """This function retrieves data of the post
.
363 :returns
: guid of post whose data was fetched
365 if self.id: id = self.id
366 if self.guid: id = self.guid
367 request = self._connection.get('posts/{0}.json'.format(id))
368 if request.status_code != 200:
369 raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
371 self.data = request.json()
374 def _fetchcomments(self):
375 """Retreives comments
for this post
.
377 if self.id: id = self.id
378 if self.guid: id = self.guid
379 if self['interactions']['comments_count']:
380 request = self._connection.get('posts/{0}/comments.json'.format(id))
381 if request.status_code != 200:
382 raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
384 self.comments = [Comment(c) for c in request.json()]
387 """Updates post data
.
390 self._fetchcomments()
393 """This function likes a post
.
394 It abstracts the
'Like' functionality
.
396 :returns
: dict -- json formatted like
object.
398 data = {'authenticity_token': repr(self._connection)}
400 request = self._connection.post('posts/{0}/likes'.format(self.id),
402 headers={'accept': 'application/json'})
404 if request.status_code != 201:
405 raise errors.PostError('{0}: Post could not be liked.'
406 .format(request.status_code))
407 return request.json()
410 """This function reshares a post
412 data = {'root_guid': self.data['guid'],
413 'authenticity_token': repr(self._connection)}
415 request = self._connection.post('reshares',
417 headers={'accept': 'application/json'})
418 if request.status_code != 201:
419 raise Exception('{0}: Post could not be reshared'.format(request.status_code))
420 return request.json()
422 def comment(self, text):
423 """This function comments on a post
425 :param text
: text to comment
.
428 data = {'text': text,
429 'authenticity_token': repr(self._connection)}
430 request = self._connection.post('posts/{0}/comments'.format(self.id),
432 headers={'accept': 'application/json'})
434 if request.status_code != 201:
435 raise Exception('{0}: Comment could not be posted.'
436 .format(request.status_code))
437 return request.json()
440 """ This function deletes this post
442 data = {'authenticity_token': repr(self._connection)}
443 request = self._connection.delete('posts/{0}'.format(self.id),
445 headers={'accept': 'application/json'})
446 if request.status_code != 204:
447 raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
449 def delete_comment(self, comment_id):
450 """This function removes a comment
from a post
452 :param comment_id
: id of the comment to remove
.
453 :type comment_id
: str
455 data = {'authenticity_token': repr(self._connection)}
456 request = self._connection.delete('posts/{0}/comments/{1}'
457 .format(self.id, comment_id),
459 headers={'accept': 'application/json'})
461 if request.status_code != 204:
462 raise errors.PostError('{0}: Comment could not be deleted'
463 .format(request.status_code))
465 def delete_like(self):
466 """This function removes a like
from a post
468 data = {'authenticity_token': self._connection.get_token()}
469 url = 'posts/{0}/likes/{1}'.format(self.id, self.data['interactions']['likes'][0]['id'])
470 request = self._connection.delete(url, data=data)
471 if request.status_code != 204:
472 raise errors.PostError('{0}: Like could not be removed.'
473 .format(request.status_code))
475 def author(self, key='name'):
476 """Returns author of the post
.
477 :param key
: all keys available
in data
['author']
479 return self.data['author'][key]