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 | """ |
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 |
138 | class 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 |
160 | class 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()] |