4410c4f24614d37375d5d8513d3da1a8677f2746
[diaspy.git] / diaspy / people.py
1 #!/usr/bin/env python3
2
3 import json
4 import re
5 import warnings
6
7 from diaspy.streams import Outer
8 from diaspy.models import Aspect
9 from diaspy import errors
10 from diaspy import search
11
12
13 def 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:
19 raise errors.InvalidHandleError('{0}'.format(handle))
20 handle = handle.split('@')
21 pod, user = handle[1], handle[0]
22 return (pod, user)
23
24
25 class User():
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.
30
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
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
36 will be notified with appropriate exception message.
37
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.
44 """
45 @classmethod
46 def parse(cls, connection, data):
47 person = data.get('person')
48 if person is None:
49 raise errors.KeyMissingFromFetchedData('person', data)
50
51 guid = person.get('guid')
52 if guid is None:
53 raise errors.KeyMissingFromFetchedData('guid', person)
54
55 handle = person.get('diaspora_id')
56 if handle is None:
57 raise errors.KeyMissingFromFetchedData('diaspora_id', person)
58
59 person_id = person.get('id')
60 if person_id is None:
61 raise errors.KeyMissingFromFetchedData('id', person)
62
63 return User(connection, guid, handle, id)
64
65 def __init__(self, connection, guid='', handle='', fetch='posts', id=0):
66 self._connection = connection
67 self.stream = []
68 self.data = {
69 'guid': guid,
70 'handle': handle,
71 'id': id,
72 }
73 self._fetch(fetch)
74
75 def __getitem__(self, key):
76 return self.data[key]
77
78 def __str__(self):
79 return self['guid']
80
81 def __repr__(self):
82 return '{0} ({1})'.format(self['handle'], self['guid'])
83
84 def _fetchstream(self):
85 self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid']))
86
87 def _fetch(self, fetch):
88 """Fetch user posts or data.
89 """
90 if fetch == 'posts':
91 if self['handle'] and not self['guid']: self.fetchhandle()
92 else: self.fetchguid()
93 elif fetch == 'data' and self['handle']:
94 self.fetchprofile()
95
96 def _finalize_data(self, data):
97 """Adjustments are needed to have similar results returned
98 by search feature and fetchguid()/fetchhandle().
99 """
100 names = [('id', 'id'),
101 ('guid', 'guid'),
102 ('name', 'name'),
103 ('avatar', 'avatar'),
104 ('handle', 'diaspora_id'),
105 ]
106 final = {}
107 for f, d in names:
108 final[f] = data[d]
109 return final
110
111 def _postproc(self, request):
112 """Makes necessary modifications to user data and
113 sets up a stream.
114
115 :param request: request object
116 :type request: request
117 """
118 if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
119 request = request.json()
120 if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze')
121 self.data = self._finalize_data(request[0]['author'])
122
123 def fetchhandle(self, protocol='https'):
124 """Fetch user data and posts using Diaspora handle.
125 """
126 pod, user = sephandle(self['handle'])
127 request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
128 self._postproc(request)
129 self._fetchstream()
130
131 def fetchguid(self):
132 """Fetch user data and posts using guid.
133 """
134 if self['guid']:
135 request = self._connection.get('people/{0}.json'.format(self['guid']))
136 self._postproc(request)
137 self._fetchstream()
138 else:
139 raise errors.UserError('GUID not set')
140
141 def fetchprofile(self):
142 """Fetches user data.
143 """
144 data = search.Search(self._connection).user(self['handle'])
145 if not data:
146 raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod))
147 else:
148 self.data = data[0]
149
150 def getHCard(self):
151 """Returns XML string containing user HCard.
152 """
153 request = self._connection.get('hcard/users/{0}'.format(self['guid']))
154 if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
155 return request.text
156
157
158 class Me():
159 """Object represetnting current user.
160 """
161 _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
162 _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
163
164 def __init__(self, connection):
165 self._connection = connection
166
167 def getInfo(self):
168 """This function returns the current user's attributes.
169
170 :returns: dict
171 """
172 request = self._connection.get('bookmarklet')
173 userdata = self._userinfo_regex.search(request.text)
174 if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
175 if userdata is None: raise errors.DiaspyError('cannot find user data')
176 userdata = userdata.group(1)
177 return json.loads(userdata)
178
179
180 class Contacts():
181 """This class represents user's list of contacts.
182 """
183 def __init__(self, connection):
184 self._connection = connection
185
186 def add(self, user_id, aspect_ids):
187 """Add user to aspects of given ids.
188
189 :param user_id: user guid
190 :type user_id: str
191 :param aspect_ids: list of aspect ids
192 :type aspect_ids: list
193 """
194 for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id)
195
196 def remove(self, user_id, aspect_ids):
197 """Remove user from aspects of given ids.
198
199 :param user_id: user guid
200 :type user_id: str
201 :param aspect_ids: list of aspect ids
202 :type aspect_ids: list
203 """
204 for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
205
206 def get(self, set=''):
207 """Returns list of user contacts.
208 Contact is a User() who is in one or more of user's
209 aspects.
210
211 By default, it will return list of users who are in
212 user's aspects.
213
214 If `set` is `all` it will also include users who only share
215 with logged user and are not in his/hers aspects.
216
217 If `set` is `only_sharing` it will return users who are only
218 sharing with logged user and ARE NOT in his/hers aspects.
219
220 :param set: if passed could be 'all' or 'only_sharing'
221 :type set: str
222 """
223 params = {}
224 if set: params['set'] = set
225
226 request = self._connection.get('contacts.json', params=params)
227 if request.status_code != 200:
228 raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
229 return [User.parse(self._connection, each) for each in request.json()]