Fix removing users from aspects, fixes #13
[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.data.get('guid', '<guid missing>')
80
81 def __repr__(self):
82 return '{0} ({1})'.format(self.handle(), self.guid())
83
84 def handle(self):
85 return self.data.get('diaspora_id', 'Unknown handle')
86
87 def guid(self):
88 return self.data.get('guid', '<guid missing>')
89
90 def _fetchstream(self):
91 self.stream = Outer(self._connection, guid=self['guid'])
92
93 def _fetch(self, fetch):
94 """Fetch user posts or data.
95 """
96 if fetch == 'posts':
97 if self['handle'] and not self['guid']: self.fetchhandle()
98 else: self.fetchguid()
99 elif fetch == 'data' and self['handle']:
100 self.fetchprofile()
101
102 def _finalize_data(self, data):
103 """Adjustments are needed to have similar results returned
104 by search feature and fetchguid()/fetchhandle().
105 """
106 return data
107
108 def _postproc(self, request):
109 """Makes necessary modifications to user data and
110 sets up a stream.
111
112 :param request: request object
113 :type request: request
114 """
115 if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
116 data = request.json()
117 self.data = self._finalize_data(data)
118
119 def fetchhandle(self, protocol='https'):
120 """Fetch user data and posts using Diaspora handle.
121 """
122 pod, user = sephandle(self['handle'])
123 request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
124 self._postproc(request)
125 self._fetchstream()
126
127 def fetchguid(self):
128 """Fetch user data and posts using guid.
129 """
130 if self['guid']:
131 request = self._connection.get('people/{0}.json'.format(self['guid']))
132 self._postproc(request)
133 self._fetchstream()
134 else:
135 raise errors.UserError('GUID not set')
136
137 def fetchprofile(self):
138 """Fetches user data.
139 """
140 data = search.Search(self._connection).user(self['handle'])
141 if not data:
142 raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod))
143 else:
144 self.data = data[0]
145
146 def aspectMemberships(self):
147 return self.data.get('contact', {}).get('aspect_memberships', [])
148
149 def getHCard(self):
150 """Returns XML string containing user HCard.
151 """
152 request = self._connection.get('hcard/users/{0}'.format(self['guid']))
153 if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
154 return request.text
155
156
157 class Me():
158 """Object represetnting current user.
159 """
160 _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
161 _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
162
163 def __init__(self, connection):
164 self._connection = connection
165
166 def getInfo(self):
167 """This function returns the current user's attributes.
168
169 :returns: dict
170 """
171 request = self._connection.get('bookmarklet')
172 userdata = self._userinfo_regex.search(request.text)
173 if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
174 if userdata is None: raise errors.DiaspyError('cannot find user data')
175 userdata = userdata.group(1)
176 return json.loads(userdata)
177
178
179 class Contacts():
180 """This class represents user's list of contacts.
181 """
182 def __init__(self, connection):
183 self._connection = connection
184
185 def add(self, user_id, aspect_ids):
186 """Add user to aspects of given ids.
187
188 :param user_id: user guid
189 :type user_id: str
190 :param aspect_ids: list of aspect ids
191 :type aspect_ids: list
192 """
193 for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id)
194
195 def remove(self, user_id, aspect_ids):
196 """Remove user from aspects of given ids.
197
198 :param user_id: user guid
199 :type user_id: str
200 :param aspect_ids: list of aspect ids
201 :type aspect_ids: list
202 """
203 for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
204
205 def get(self, set=''):
206 """Returns list of user contacts.
207 Contact is a User() who is in one or more of user's
208 aspects.
209
210 By default, it will return list of users who are in
211 user's aspects.
212
213 If `set` is `all` it will also include users who only share
214 with logged user and are not in his/hers aspects.
215
216 If `set` is `only_sharing` it will return users who are only
217 sharing with logged user and ARE NOT in his/hers aspects.
218
219 :param set: if passed could be 'all' or 'only_sharing'
220 :type set: str
221 """
222 params = {}
223 if set: params['set'] = set
224
225 request = self._connection.get('contacts.json', params=params)
226 if request.status_code != 200:
227 raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
228 return [User.parse(self._connection, each) for each in request.json()]