Merge branch 'devel'
[diaspy.git] / diaspy / people.py
CommitLineData
74edacee
MM
1#!/usr/bin/env python3
2
3import json
beaa09fb 4import re
03ffc9ea
MM
5import warnings
6
beaa09fb 7from diaspy.streams import Outer
d589deff 8from diaspy.models import Aspect
65b1f099 9from diaspy import errors
7c6fbe5b 10from diaspy import search
beaa09fb
MM
11
12
6d8d47ce
MM
13def sephandle(handle):
14 """Separate Diaspora* handle into pod pod and user.
15
16 :returns: two-tuple (pod, user)
17 """
18 if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
615edb73 19 raise errors.InvalidHandleError('{0}'.format(handle))
6d8d47ce
MM
20 handle = handle.split('@')
21 pod, user = handle[1], handle[0]
22 return (pod, user)
23
24
488d7ff6 25class User():
beaa09fb
MM
26 """This class abstracts a D* user.
27 This object goes around the limitations of current D* API and will
28 extract user data using black magic.
29 However, no chickens are harmed when you use it.
17d8f406 30
7b99bf75
JR
31 The parameter fetch should be either 'posts', 'data' or 'none'. By
32 default it is 'posts' which means in addition to user data, stream
3cf4514e
MM
33 will be fetched. If user has not posted yet diaspy will not be able
34 to extract the information from his/her posts. Since there is no official
35 way to do it we rely on user posts. If this will be the case user
7b99bf75 36 will be notified with appropriate exception message.
3cf4514e 37
7b99bf75
JR
38 If fetch is 'data', only user data will be fetched. If the user is
39 not found, no exception will be returned.
40
41 When creating new User() one can pass either guid, handle and/or id as
42 optional parameters. GUID takes precedence over handle when fetching
43 user stream. When fetching user data, handle is required.
beaa09fb 44 """
7b99bf75 45 def __init__(self, connection, guid='', handle='', fetch='posts', id=0):
beaa09fb 46 self._connection = connection
6cd1bae0 47 self.stream = []
b9fb4030
JR
48 self.data = {
49 'guid': guid,
50 'handle': handle,
1e192d06 51 'id': id,
b9fb4030 52 }
9aa1c960 53 self._fetch(fetch)
beaa09fb
MM
54
55 def __getitem__(self, key):
56 return self.data[key]
57
1e192d06
MM
58 def __str__(self):
59 return self['guid']
60
61 def __repr__(self):
e21587b9 62 return '{0} ({1})'.format(self['handle'], self['guid'])
1e192d06 63
415337b6
MM
64 def _fetchstream(self):
65 self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid']))
66
9aa1c960
MM
67 def _fetch(self, fetch):
68 """Fetch user posts or data.
69 """
b9fb4030 70 if fetch == 'posts':
f50cbea3 71 if self['handle'] and not self['guid']: self.fetchhandle()
9aa1c960 72 else: self.fetchguid()
f50cbea3 73 elif fetch == 'data' and self['handle']:
1f779f83 74 self.fetchprofile()
b9fb4030 75
7c6fbe5b 76 def _finalize_data(self, data):
6d8d47ce 77 """Adjustments are needed to have similar results returned
f50cbea3 78 by search feature and fetchguid()/fetchhandle().
6d8d47ce 79 """
f50cbea3
MM
80 names = [('id', 'id'),
81 ('guid', 'guid'),
82 ('name', 'name'),
83 ('avatar', 'avatar'),
84 ('handle', 'diaspora_id'),
85 ]
7b99bf75 86 final = {}
6d8d47ce 87 for f, d in names:
7b99bf75
JR
88 final[f] = data[d]
89 return final
beaa09fb 90
a8fdc14a
MM
91 def _postproc(self, request):
92 """Makes necessary modifications to user data and
93 sets up a stream.
94
95 :param request: request object
96 :type request: request
beaa09fb 97 """
6d8d47ce 98 if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
f50cbea3 99 request = request.json()
65b1f099 100 if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze')
7c6fbe5b 101 self.data = self._finalize_data(request[0]['author'])
beaa09fb 102
1f779f83 103 def fetchhandle(self, protocol='https'):
7b99bf75 104 """Fetch user data and posts using Diaspora handle.
beaa09fb 105 """
f50cbea3 106 pod, user = sephandle(self['handle'])
73a9e0d3 107 request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
a8fdc14a 108 self._postproc(request)
415337b6 109 self._fetchstream()
beaa09fb 110
1f779f83 111 def fetchguid(self):
7b99bf75 112 """Fetch user data and posts using guid.
beaa09fb 113 """
f50cbea3
MM
114 if self['guid']:
115 request = self._connection.get('people/{0}.json'.format(self['guid']))
3311aa0c 116 self._postproc(request)
415337b6 117 self._fetchstream()
3311aa0c
MM
118 else:
119 raise errors.UserError('GUID not set')
3cf4514e 120
5131bd9e
MM
121 def fetchprofile(self):
122 """Fetches user data.
123 """
03ffc9ea
MM
124 data = search.Search(self._connection).user(self['handle'])
125 if not data:
0cbdffd5 126 raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod))
03ffc9ea
MM
127 else:
128 self.data = data[0]
5131bd9e 129
39af9756
MM
130 def getHCard(self):
131 """Returns XML string containing user HCard.
132 """
133 request = self._connection.get('hcard/users/{0}'.format(self['guid']))
134 if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
135 return request.text
136
dd0a4d9f 137
f8752360
MM
138class Me():
139 """Object represetnting current user.
140 """
141 _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
142 _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
143
144 def __init__(self, connection):
145 self._connection = connection
146
39af9756 147 def getInfo(self):
f8752360
MM
148 """This function returns the current user's attributes.
149
1e578db9 150 :returns: dict
f8752360
MM
151 """
152 request = self._connection.get('bookmarklet')
153 userdata = self._userinfo_regex.search(request.text)
154 if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
155 if userdata is None: raise errors.DiaspyError('cannot find user data')
156 userdata = userdata.group(1)
157 return json.loads(userdata)
158
159
dd0a4d9f
MM
160class Contacts():
161 """This class represents user's list of contacts.
162 """
163 def __init__(self, connection):
164 self._connection = connection
165
d589deff
MM
166 def add(self, user_id, aspect_ids):
167 """Add user to aspects of given ids.
dd0a4d9f 168
d589deff
MM
169 :param user_id: user guid
170 :type user_id: str
171 :param aspect_ids: list of aspect ids
172 :type aspect_ids: list
dd0a4d9f 173 """
7a818fdb 174 for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id)
27f09973 175
d589deff
MM
176 def remove(self, user_id, aspect_ids):
177 """Remove user from aspects of given ids.
27f09973 178
d589deff
MM
179 :param user_id: user guid
180 :type user_id: str
181 :param aspect_ids: list of aspect ids
182 :type aspect_ids: list
27f09973 183 """
7a818fdb 184 for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
27f09973 185
d589deff 186 def get(self, set=''):
27f09973 187 """Returns list of user contacts.
d589deff
MM
188 Contact is a User() who is in one or more of user's
189 aspects.
190
7a818fdb
MM
191 By default, it will return list of users who are in
192 user's aspects.
193
d589deff
MM
194 If `set` is `all` it will also include users who only share
195 with logged user and are not in his/hers aspects.
7a818fdb 196
d589deff
MM
197 If `set` is `only_sharing` it will return users who are only
198 sharing with logged user and ARE NOT in his/hers aspects.
199
200 :param set: if passed could be 'all' or 'only_sharing'
201 :type set: str
27f09973 202 """
d589deff
MM
203 params = {}
204 if set: params['set'] = set
205
206 request = self._connection.get('contacts.json', params=params)
27f09973
MM
207 if request.status_code != 200:
208 raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
f50cbea3 209 return [User(self._connection, guid=user['guid'], handle=user['handle'], fetch=None) for user in request.json()]