Commit | Line | Data |
---|---|---|
74edacee MM |
1 | #!/usr/bin/env python3 |
2 | ||
3 | import json | |
beaa09fb | 4 | import re |
03ffc9ea MM |
5 | import warnings |
6 | ||
beaa09fb | 7 | from diaspy.streams import Outer |
d589deff | 8 | from diaspy.models import Aspect |
65b1f099 | 9 | from diaspy import errors |
7c6fbe5b | 10 | from diaspy import search |
beaa09fb MM |
11 | |
12 | ||
6d8d47ce MM |
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: | |
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 | 25 | class 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 | """ |
d84905af MM |
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 | ||
7b99bf75 | 65 | def __init__(self, connection, guid='', handle='', fetch='posts', id=0): |
beaa09fb | 66 | self._connection = connection |
6cd1bae0 | 67 | self.stream = [] |
b9fb4030 JR |
68 | self.data = { |
69 | 'guid': guid, | |
70 | 'handle': handle, | |
1e192d06 | 71 | 'id': id, |
b9fb4030 | 72 | } |
9aa1c960 | 73 | self._fetch(fetch) |
beaa09fb MM |
74 | |
75 | def __getitem__(self, key): | |
76 | return self.data[key] | |
77 | ||
1e192d06 | 78 | def __str__(self): |
0f452d5d | 79 | return self.data.get('guid', '<guid missing>') |
1e192d06 MM |
80 | |
81 | def __repr__(self): | |
0f452d5d | 82 | return '{0} ({1})'.format(self.handle(), self.guid()) |
1e192d06 | 83 | |
87cf6f93 MM |
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 | ||
e2c368a8 MM |
90 | def id(self): |
91 | return self.data['id'] | |
92 | ||
415337b6 | 93 | def _fetchstream(self): |
e4c9633a | 94 | self.stream = Outer(self._connection, guid=self['guid']) |
415337b6 | 95 | |
9aa1c960 MM |
96 | def _fetch(self, fetch): |
97 | """Fetch user posts or data. | |
98 | """ | |
b9fb4030 | 99 | if fetch == 'posts': |
f50cbea3 | 100 | if self['handle'] and not self['guid']: self.fetchhandle() |
9aa1c960 | 101 | else: self.fetchguid() |
f50cbea3 | 102 | elif fetch == 'data' and self['handle']: |
1f779f83 | 103 | self.fetchprofile() |
b9fb4030 | 104 | |
7c6fbe5b | 105 | def _finalize_data(self, data): |
6d8d47ce | 106 | """Adjustments are needed to have similar results returned |
f50cbea3 | 107 | by search feature and fetchguid()/fetchhandle(). |
6d8d47ce | 108 | """ |
d4f2d9ac | 109 | return data |
beaa09fb | 110 | |
a8fdc14a MM |
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 | |
beaa09fb | 117 | """ |
6d8d47ce | 118 | if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code)) |
d4f2d9ac MM |
119 | data = request.json() |
120 | self.data = self._finalize_data(data) | |
beaa09fb | 121 | |
1f779f83 | 122 | def fetchhandle(self, protocol='https'): |
7b99bf75 | 123 | """Fetch user data and posts using Diaspora handle. |
beaa09fb | 124 | """ |
f50cbea3 | 125 | pod, user = sephandle(self['handle']) |
73a9e0d3 | 126 | request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True) |
a8fdc14a | 127 | self._postproc(request) |
415337b6 | 128 | self._fetchstream() |
beaa09fb | 129 | |
5ef21061 C |
130 | def fetchguid(self, fetch_stream=True): |
131 | """Fetch user data and posts (if fetch_stream is True) using guid. | |
beaa09fb | 132 | """ |
f50cbea3 MM |
133 | if self['guid']: |
134 | request = self._connection.get('people/{0}.json'.format(self['guid'])) | |
3311aa0c | 135 | self._postproc(request) |
5ef21061 | 136 | if fetch_stream: self._fetchstream() |
3311aa0c MM |
137 | else: |
138 | raise errors.UserError('GUID not set') | |
3cf4514e | 139 | |
5131bd9e MM |
140 | def fetchprofile(self): |
141 | """Fetches user data. | |
142 | """ | |
03ffc9ea MM |
143 | data = search.Search(self._connection).user(self['handle']) |
144 | if not data: | |
0cbdffd5 | 145 | raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod)) |
03ffc9ea MM |
146 | else: |
147 | self.data = data[0] | |
5131bd9e | 148 | |
564efb9e MM |
149 | def aspectMemberships(self): |
150 | return self.data.get('contact', {}).get('aspect_memberships', []) | |
151 | ||
39af9756 MM |
152 | def getHCard(self): |
153 | """Returns XML string containing user HCard. | |
154 | """ | |
155 | request = self._connection.get('hcard/users/{0}'.format(self['guid'])) | |
156 | if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid'])) | |
157 | return request.text | |
158 | ||
dd0a4d9f | 159 | |
f8752360 MM |
160 | class Me(): |
161 | """Object represetnting current user. | |
162 | """ | |
163 | _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})') | |
164 | _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads') | |
165 | ||
166 | def __init__(self, connection): | |
167 | self._connection = connection | |
168 | ||
39af9756 | 169 | def getInfo(self): |
f8752360 MM |
170 | """This function returns the current user's attributes. |
171 | ||
1e578db9 | 172 | :returns: dict |
f8752360 MM |
173 | """ |
174 | request = self._connection.get('bookmarklet') | |
175 | userdata = self._userinfo_regex.search(request.text) | |
176 | if userdata is None: userdata = self._userinfo_regex_2.search(request.text) | |
177 | if userdata is None: raise errors.DiaspyError('cannot find user data') | |
178 | userdata = userdata.group(1) | |
179 | return json.loads(userdata) | |
180 | ||
181 | ||
dd0a4d9f MM |
182 | class Contacts(): |
183 | """This class represents user's list of contacts. | |
184 | """ | |
185 | def __init__(self, connection): | |
186 | self._connection = connection | |
187 | ||
d589deff MM |
188 | def add(self, user_id, aspect_ids): |
189 | """Add user to aspects of given ids. | |
dd0a4d9f | 190 | |
d589deff MM |
191 | :param user_id: user guid |
192 | :type user_id: str | |
193 | :param aspect_ids: list of aspect ids | |
194 | :type aspect_ids: list | |
dd0a4d9f | 195 | """ |
7a818fdb | 196 | for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id) |
27f09973 | 197 | |
d589deff MM |
198 | def remove(self, user_id, aspect_ids): |
199 | """Remove user from aspects of given ids. | |
27f09973 | 200 | |
d589deff MM |
201 | :param user_id: user guid |
202 | :type user_id: str | |
203 | :param aspect_ids: list of aspect ids | |
204 | :type aspect_ids: list | |
27f09973 | 205 | """ |
7a818fdb | 206 | for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id) |
27f09973 | 207 | |
d589deff | 208 | def get(self, set=''): |
27f09973 | 209 | """Returns list of user contacts. |
d589deff MM |
210 | Contact is a User() who is in one or more of user's |
211 | aspects. | |
212 | ||
7a818fdb MM |
213 | By default, it will return list of users who are in |
214 | user's aspects. | |
215 | ||
d589deff MM |
216 | If `set` is `all` it will also include users who only share |
217 | with logged user and are not in his/hers aspects. | |
7a818fdb | 218 | |
d589deff MM |
219 | If `set` is `only_sharing` it will return users who are only |
220 | sharing with logged user and ARE NOT in his/hers aspects. | |
221 | ||
222 | :param set: if passed could be 'all' or 'only_sharing' | |
223 | :type set: str | |
27f09973 | 224 | """ |
d589deff MM |
225 | params = {} |
226 | if set: params['set'] = set | |
227 | ||
228 | request = self._connection.get('contacts.json', params=params) | |
27f09973 MM |
229 | if request.status_code != 200: |
230 | raise Exception('status code {0}: cannot get contacts'.format(request.status_code)) | |
d84905af | 231 | return [User.parse(self._connection, each) for each in request.json()] |