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 MM |
78 | def __str__(self): |
79 | return self['guid'] | |
80 | ||
81 | def __repr__(self): | |
e21587b9 | 82 | return '{0} ({1})'.format(self['handle'], self['guid']) |
1e192d06 | 83 | |
415337b6 MM |
84 | def _fetchstream(self): |
85 | self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid'])) | |
86 | ||
9aa1c960 MM |
87 | def _fetch(self, fetch): |
88 | """Fetch user posts or data. | |
89 | """ | |
b9fb4030 | 90 | if fetch == 'posts': |
f50cbea3 | 91 | if self['handle'] and not self['guid']: self.fetchhandle() |
9aa1c960 | 92 | else: self.fetchguid() |
f50cbea3 | 93 | elif fetch == 'data' and self['handle']: |
1f779f83 | 94 | self.fetchprofile() |
b9fb4030 | 95 | |
7c6fbe5b | 96 | def _finalize_data(self, data): |
6d8d47ce | 97 | """Adjustments are needed to have similar results returned |
f50cbea3 | 98 | by search feature and fetchguid()/fetchhandle(). |
6d8d47ce | 99 | """ |
f50cbea3 MM |
100 | names = [('id', 'id'), |
101 | ('guid', 'guid'), | |
102 | ('name', 'name'), | |
103 | ('avatar', 'avatar'), | |
104 | ('handle', 'diaspora_id'), | |
105 | ] | |
7b99bf75 | 106 | final = {} |
6d8d47ce | 107 | for f, d in names: |
7b99bf75 JR |
108 | final[f] = data[d] |
109 | return final | |
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)) |
f50cbea3 | 119 | request = request.json() |
65b1f099 | 120 | if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze') |
7c6fbe5b | 121 | self.data = self._finalize_data(request[0]['author']) |
beaa09fb | 122 | |
1f779f83 | 123 | def fetchhandle(self, protocol='https'): |
7b99bf75 | 124 | """Fetch user data and posts using Diaspora handle. |
beaa09fb | 125 | """ |
f50cbea3 | 126 | pod, user = sephandle(self['handle']) |
73a9e0d3 | 127 | request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True) |
a8fdc14a | 128 | self._postproc(request) |
415337b6 | 129 | self._fetchstream() |
beaa09fb | 130 | |
1f779f83 | 131 | def fetchguid(self): |
7b99bf75 | 132 | """Fetch user data and posts using guid. |
beaa09fb | 133 | """ |
f50cbea3 MM |
134 | if self['guid']: |
135 | request = self._connection.get('people/{0}.json'.format(self['guid'])) | |
3311aa0c | 136 | self._postproc(request) |
415337b6 | 137 | self._fetchstream() |
3311aa0c MM |
138 | else: |
139 | raise errors.UserError('GUID not set') | |
3cf4514e | 140 | |
5131bd9e MM |
141 | def fetchprofile(self): |
142 | """Fetches user data. | |
143 | """ | |
03ffc9ea MM |
144 | data = search.Search(self._connection).user(self['handle']) |
145 | if not data: | |
0cbdffd5 | 146 | raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod)) |
03ffc9ea MM |
147 | else: |
148 | self.data = data[0] | |
5131bd9e | 149 | |
39af9756 MM |
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 | ||
dd0a4d9f | 157 | |
f8752360 MM |
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 | ||
39af9756 | 167 | def getInfo(self): |
f8752360 MM |
168 | """This function returns the current user's attributes. |
169 | ||
1e578db9 | 170 | :returns: dict |
f8752360 MM |
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 | ||
dd0a4d9f MM |
180 | class Contacts(): |
181 | """This class represents user's list of contacts. | |
182 | """ | |
183 | def __init__(self, connection): | |
184 | self._connection = connection | |
185 | ||
d589deff MM |
186 | def add(self, user_id, aspect_ids): |
187 | """Add user to aspects of given ids. | |
dd0a4d9f | 188 | |
d589deff MM |
189 | :param user_id: user guid |
190 | :type user_id: str | |
191 | :param aspect_ids: list of aspect ids | |
192 | :type aspect_ids: list | |
dd0a4d9f | 193 | """ |
7a818fdb | 194 | for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id) |
27f09973 | 195 | |
d589deff MM |
196 | def remove(self, user_id, aspect_ids): |
197 | """Remove user from aspects of given ids. | |
27f09973 | 198 | |
d589deff MM |
199 | :param user_id: user guid |
200 | :type user_id: str | |
201 | :param aspect_ids: list of aspect ids | |
202 | :type aspect_ids: list | |
27f09973 | 203 | """ |
7a818fdb | 204 | for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id) |
27f09973 | 205 | |
d589deff | 206 | def get(self, set=''): |
27f09973 | 207 | """Returns list of user contacts. |
d589deff MM |
208 | Contact is a User() who is in one or more of user's |
209 | aspects. | |
210 | ||
7a818fdb MM |
211 | By default, it will return list of users who are in |
212 | user's aspects. | |
213 | ||
d589deff MM |
214 | If `set` is `all` it will also include users who only share |
215 | with logged user and are not in his/hers aspects. | |
7a818fdb | 216 | |
d589deff MM |
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 | |
27f09973 | 222 | """ |
d589deff MM |
223 | params = {} |
224 | if set: params['set'] = set | |
225 | ||
226 | request = self._connection.get('contacts.json', params=params) | |
27f09973 MM |
227 | if request.status_code != 200: |
228 | raise Exception('status code {0}: cannot get contacts'.format(request.status_code)) | |
d84905af | 229 | return [User.parse(self._connection, each) for each in request.json()] |