8 from diaspy
.streams
import Outer
9 from diaspy
.models
import Aspect
10 from diaspy
import errors
11 from diaspy
import search
14 def sephandle(handle
):
15 """Separate Diaspora* handle into pod pod and user.
17 :returns: two-tuple (pod, user)
19 if re
.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle
) is None:
20 raise errors
.InvalidHandleError('{0}'.format(handle
))
21 handle
= handle
.split('@')
22 pod
, user
= handle
[1], handle
[0]
27 """This class abstracts a D* user.
28 This object goes around the limitations of current D* API and will
29 extract user data using black magic.
30 However, no chickens are harmed when you use it.
32 The parameter fetch should be either 'posts', 'data' or 'none'. By
33 default it is 'posts' which means in addition to user data, stream
34 will be fetched. If user has not posted yet diaspy will not be able
35 to extract the information from his/her posts. Since there is no official
36 way to do it we rely on user posts. If this will be the case user
37 will be notified with appropriate exception message.
39 If fetch is 'data', only user data will be fetched. If the user is
40 not found, no exception will be returned.
42 When creating new User() one can pass either guid, handle and/or id as
43 optional parameters. GUID takes precedence over handle when fetching
44 user stream. When fetching user data, handle is required.
47 def parse(cls
, connection
, data
):
48 person
= data
.get('person')
50 raise errors
.KeyMissingFromFetchedData('person', data
)
52 guid
= person
.get('guid')
54 raise errors
.KeyMissingFromFetchedData('guid', person
)
56 handle
= person
.get('diaspora_id')
58 raise errors
.KeyMissingFromFetchedData('diaspora_id', person
)
60 person_id
= person
.get('id')
62 raise errors
.KeyMissingFromFetchedData('id', person
)
64 return User(connection
, guid
, handle
, id, data
=data
)
66 def __init__(self
, connection
, guid
='', handle
='', fetch
='posts', id=0, data
=None):
67 self
._connection
= connection
75 if data
: self
.data
.update( data
)
76 if fetch
: self
._fetch
(fetch
)
78 def __getitem__(self
, key
):
82 return self
.data
.get('guid', '<guid missing>')
85 return '{0} ({1})'.format(self
.handle(), self
.guid())
88 if 'handle' in self
.data
: return self
['handle']
89 return self
.data
.get('diaspora_id', 'Unknown handle')
92 return self
.data
.get('guid', '<guid missing>')
95 return self
.data
['id']
97 def _fetchstream(self
):
98 self
.stream
= Outer(self
._connection
, guid
=self
['guid'])
100 def _fetch(self
, fetch
):
101 """Fetch user posts or data.
104 if self
.handle() and not self
['guid']: self
.fetchhandle()
105 else: self
.fetchguid()
106 elif fetch
== 'data' and self
['handle']:
109 def _finalize_data(self
, data
):
110 """Adjustments are needed to have similar results returned
111 by search feature and fetchguid()/fetchhandle().
115 def _postproc(self
, request
):
116 """Makes necessary modifications to user data and
119 :param request: request object
120 :type request: request
122 if request
.status_code
!= 200: raise Exception('wrong error code: {0}'.format(request
.status_code
))
123 data
= request
.json()
124 self
.data
= self
._finalize
_data
(data
)
126 def fetchhandle(self
, protocol
='https'):
127 """Fetch user data and posts using Diaspora handle.
129 pod
, user
= sephandle(self
['handle'])
130 request
= self
._connection
.get('{0}://{1}/u/{2}.json'.format(protocol
, pod
, user
), direct
=True)
131 self
._postproc
(request
)
134 def fetchguid(self
, fetch_stream
=True):
135 """Fetch user data and posts (if fetch_stream is True) using guid.
138 request
= self
._connection
.get('people/{0}.json'.format(self
['guid']))
139 self
._postproc
(request
)
140 if fetch_stream
: self
._fetchstream
()
142 raise errors
.UserError('GUID not set')
144 def fetchprofile(self
):
145 """Fetches user data.
147 data
= search
.Search(self
._connection
).user(self
.handle())
149 raise errors
.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self
.handle(), self
._connection
.pod
))
151 self
.data
.update( data
[0] )
153 def aspectMemberships(self
):
154 if 'contact' in self
.data
:
155 return self
.data
.get('contact', {}).get('aspect_memberships', [])
157 return self
.data
.get('aspect_memberships', [])
161 --> GET /people/{GUID}/photos.json HTTP/1.1
169 "guid":"{photo_guid}",
170 "created_at":"2018-03-08T23:48:31.000Z",
173 "guid":"{author_guid}",
174 "name":"{author_name}",
175 "diaspora_id":"{diaspora_id}",
176 "avatar":{"small":"{avatar_url_small}","medium":"{avatar_url_medium}","large":"{avatar_url_large}"}
179 "small":"{photo_url}",
180 "medium":"{photo_url}",
181 "large":"{photo_url}"
183 "dimensions":{"height":847,"width":998},
190 if there are no photo's it returns:
194 request
= self
._connection
.get('/people/{0}/photos.json'.format(self
['guid']))
195 if request
.status_code
!= 200: raise errors
.UserError('could not fetch photos for user: {0}'.format(self
['guid']))
197 json
= request
.json()
198 if json
: self
.photos
= json
['photos']
199 return json
['photos']
202 """Returns json containing user HCard.
203 --> /people/{guid}/hovercard.json?_={timestamp}
208 "guid":"1234567890abcdef",
210 "diaspora_id":"batman@test.test",
213 "avatar":"https://nicetesturl.url/image.jpg",
214 "tags":["tag1", "tag2", "tag3", "tag4", "tag5"]}
217 timestamp
= int(time
.mktime(time
.gmtime()))
218 request
= self
._connection
.get('/people/{0}/hovercard.json?_={}'.format(self
['guid'], timestamp
))
219 if request
.status_code
!= 200: raise errors
.UserError('could not fetch hcard for user: {0}'.format(self
['guid']))
220 return request
.json()
222 def deletePhoto(self
, photo_id
):
224 --> DELETE /photos/{PHOTO_ID} HTTP/1.1
225 <-- HTTP/1.1 204 No Content
227 request
= self
._connection
.delete('/photos/{0}'.format(photo_id
))
228 if request
.status_code
!= 204: raise errors
.UserError('could not delete photo_id: {0}'.format(photo_id
))
231 """Object represetnting current user.
233 _userinfo_regex
= re
.compile(r
'window.current_user_attributes = ({.*})')
234 _userinfo_regex_2
= re
.compile(r
'gon.user=({.*});gon.preloads')
236 def __init__(self
, connection
):
237 self
._connection
= connection
240 """This function returns the current user's attributes.
244 request
= self
._connection
.get('bookmarklet')
245 userdata
= self
._userinfo
_regex
.search(request
.text
)
246 if userdata
is None: userdata
= self
._userinfo
_regex
_2.search(request
.text
)
247 if userdata
is None: raise errors
.DiaspyError('cannot find user data')
248 userdata
= userdata
.group(1)
249 return json
.loads(userdata
)
253 """This class represents user's list of contacts.
255 def __init__(self
, connection
, fetch
=False, set=''):
256 self
._connection
= connection
258 if fetch
: self
.contacts
= self
.get(set)
260 def __getitem__(self
, index
):
261 return self
.contacts
[index
]
263 def addAspect(self
, name
, visible
=False):
265 --> POST /aspects HTTP/1.1
266 --> {"person_id":null,"name":"test","contacts_visible":false}
274 :param name: aspect name to add
276 :param visible: sets if contacts in aspect are visible for each and other
278 :returns: JSON from request
283 'contacts_visible': visible
285 headers
={'content-type': 'application/json',
286 'accept': 'application/json' }
287 request
= self
._connection
.tokenFrom('contacts').post('aspects', headers
=headers
, data
=json
.dumps(data
))
289 if request
.status_code
== 400:
290 raise errors
.AspectError('duplicate record, aspect alreadt exists: {0}'.format(request
.status_code
))
291 elif request
.status_code
!= 200:
292 raise errors
.AspectError('wrong status code: {0}'.format(request
.status_code
))
294 new_aspect
= request
.json()
295 self
._connection
.userdata()['aspects'].append( new_aspect
)
299 def deleteAspect(self
, aspect_id
):
301 --> POST /aspects/{ASPECT_ID} HTTP/1.1
302 _method=delete&authenticity_token={TOKEN}
303 Content-Type: application/x-www-form-urlencoded
305 <-- HTTP/1.1 302 Found
306 Content-Type: text/html; charset=utf-8
308 request
= self
._connection
.tokenFrom('contacts').delete('aspects/{}'.format( aspect_id
))
310 if request
.status_code
!= 200: # since we don't post but delete
311 raise errors
.AspectError('wrong status code: {0}'.format(request
.status_code
))
313 def add(self
, user_id
, aspect_ids
):
314 """Add user to aspects of given ids.
316 :param user_id: user id (not guid)
318 :param aspect_ids: list of aspect ids
319 :type aspect_ids: list
321 # TODO update self.contacts
322 # Returns {"aspect_id":123,"person_id":123}
323 for aid
in aspect_ids
:
324 new_aspect_membership
= Aspect(self
._connection
, aid
).addUser(user_id
)
327 if new_aspect_membership
:
328 for user
in self
.contacts
:
329 if int(user
.data
['person_id']) == int(user_id
):
330 user
.data
['aspect_memberships'].append( new_aspect_membership
)
331 return new_aspect_membership
333 def remove(self
, user_id
, aspect_ids
):
334 """Remove user from aspects of given ids.
336 :param user_id: user id
338 :param aspect_ids: list of aspect ids
339 :type aspect_ids: list
341 for aid
in aspect_ids
: Aspect(self
._connection
, aid
).removeUser(user_id
)
343 def get(self
, set='', page
=0):
344 """Returns list of user contacts.
345 Contact is a User() who is in one or more of user's
348 By default, it will return list of users who are in
351 If `set` is `all` it will also include users who only share
352 with logged user and are not in his/hers aspects.
354 If `set` is `only_sharing` it will return users who are only
355 sharing with logged user and ARE NOT in his/hers aspects.
357 # On "All contacts" button diaspora
358 on the time of testing this I had 20 contacts and 10 that
359 where only sharing with me. So 30 in total.
361 --> GET /contacts?set=all HTTP/1.1
363 returned 25 contacts (5 only sharing with me)
365 --> GET /contacts.json?page=1&set=all&_=1524410225376 HTTP/1.1
367 returned the same list as before.
369 --> GET /contacts.json?page=2&set=all&_=1524410225377 HTTP/1.1
371 returned the other 5 that where only sharing with me.
373 --> GET /contacts.json?page=3&set=all&_=1524410225378 HTTP/1.1
377 It appears that /contacts?set=all returns a maximum of 25
380 So if /contacts?set=all returns 25 contacts then request next
381 page until page returns a list with less then 25. I don't see a
382 reason why we should request page=1 'cause the previous request
383 will be the same. So begin with page=2 if /contacts?set=all
386 :param set: if passed could be 'all' or 'only_sharing'
392 params
['_'] = int(time
.mktime(time
.gmtime()))
393 if page
: params
['page'] = page
395 request
= self
._connection
.get('contacts.json', params
=params
)
396 if request
.status_code
!= 200:
397 raise Exception('status code {0}: cannot get contacts'.format(request
.status_code
))
399 json
= request
.json()
400 users
= [User
.parse(self
._connection
, each
) for each
in json
]
402 if not page
: page
= 1
403 users
+= self
.get(set=set, page
=page
+1)