Commit | Line | Data |
---|---|---|
74edacee MM |
1 | #!/usr/bin/env python3 |
2 | ||
3 | import json | |
beaa09fb MM |
4 | import re |
5 | from diaspy.streams import Outer | |
d589deff | 6 | from diaspy.models import Aspect |
65b1f099 | 7 | from diaspy import errors |
7c6fbe5b | 8 | from diaspy import search |
beaa09fb MM |
9 | |
10 | ||
6d8d47ce MM |
11 | def sephandle(handle): |
12 | """Separate Diaspora* handle into pod pod and user. | |
13 | ||
14 | :returns: two-tuple (pod, user) | |
15 | """ | |
16 | if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None: | |
615edb73 | 17 | raise errors.InvalidHandleError('{0}'.format(handle)) |
6d8d47ce MM |
18 | handle = handle.split('@') |
19 | pod, user = handle[1], handle[0] | |
20 | return (pod, user) | |
21 | ||
22 | ||
488d7ff6 | 23 | class User(): |
beaa09fb MM |
24 | """This class abstracts a D* user. |
25 | This object goes around the limitations of current D* API and will | |
26 | extract user data using black magic. | |
27 | However, no chickens are harmed when you use it. | |
17d8f406 | 28 | |
7b99bf75 JR |
29 | The parameter fetch should be either 'posts', 'data' or 'none'. By |
30 | default it is 'posts' which means in addition to user data, stream | |
3cf4514e MM |
31 | will be fetched. If user has not posted yet diaspy will not be able |
32 | to extract the information from his/her posts. Since there is no official | |
33 | way to do it we rely on user posts. If this will be the case user | |
7b99bf75 | 34 | will be notified with appropriate exception message. |
3cf4514e | 35 | |
7b99bf75 JR |
36 | If fetch is 'data', only user data will be fetched. If the user is |
37 | not found, no exception will be returned. | |
38 | ||
39 | When creating new User() one can pass either guid, handle and/or id as | |
40 | optional parameters. GUID takes precedence over handle when fetching | |
41 | user stream. When fetching user data, handle is required. | |
beaa09fb | 42 | """ |
7b99bf75 | 43 | def __init__(self, connection, guid='', handle='', fetch='posts', id=0): |
beaa09fb | 44 | self._connection = connection |
6cd1bae0 | 45 | self.stream = [] |
b9fb4030 JR |
46 | self.data = { |
47 | 'guid': guid, | |
48 | 'handle': handle, | |
1e192d06 | 49 | 'id': id, |
b9fb4030 | 50 | } |
9aa1c960 | 51 | self._fetch(fetch) |
beaa09fb MM |
52 | |
53 | def __getitem__(self, key): | |
54 | return self.data[key] | |
55 | ||
1e192d06 MM |
56 | def __str__(self): |
57 | return self['guid'] | |
58 | ||
59 | def __repr__(self): | |
e21587b9 | 60 | return '{0} ({1})'.format(self['handle'], self['guid']) |
1e192d06 | 61 | |
415337b6 MM |
62 | def _fetchstream(self): |
63 | self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid'])) | |
64 | ||
9aa1c960 MM |
65 | def _fetch(self, fetch): |
66 | """Fetch user posts or data. | |
67 | """ | |
b9fb4030 | 68 | if fetch == 'posts': |
f50cbea3 | 69 | if self['handle'] and not self['guid']: self.fetchhandle() |
9aa1c960 | 70 | else: self.fetchguid() |
f50cbea3 | 71 | elif fetch == 'data' and self['handle']: |
1f779f83 | 72 | self.fetchprofile() |
b9fb4030 | 73 | |
7c6fbe5b | 74 | def _finalize_data(self, data): |
6d8d47ce | 75 | """Adjustments are needed to have similar results returned |
f50cbea3 | 76 | by search feature and fetchguid()/fetchhandle(). |
6d8d47ce | 77 | """ |
f50cbea3 MM |
78 | names = [('id', 'id'), |
79 | ('guid', 'guid'), | |
80 | ('name', 'name'), | |
81 | ('avatar', 'avatar'), | |
82 | ('handle', 'diaspora_id'), | |
83 | ] | |
7b99bf75 | 84 | final = {} |
6d8d47ce | 85 | for f, d in names: |
7b99bf75 JR |
86 | final[f] = data[d] |
87 | return final | |
beaa09fb | 88 | |
a8fdc14a MM |
89 | def _postproc(self, request): |
90 | """Makes necessary modifications to user data and | |
91 | sets up a stream. | |
92 | ||
93 | :param request: request object | |
94 | :type request: request | |
beaa09fb | 95 | """ |
6d8d47ce | 96 | if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code)) |
f50cbea3 | 97 | request = request.json() |
65b1f099 | 98 | if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze') |
7c6fbe5b | 99 | self.data = self._finalize_data(request[0]['author']) |
beaa09fb | 100 | |
1f779f83 | 101 | def fetchhandle(self, protocol='https'): |
7b99bf75 | 102 | """Fetch user data and posts using Diaspora handle. |
beaa09fb | 103 | """ |
f50cbea3 | 104 | pod, user = sephandle(self['handle']) |
73a9e0d3 | 105 | request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True) |
a8fdc14a | 106 | self._postproc(request) |
415337b6 | 107 | self._fetchstream() |
beaa09fb | 108 | |
1f779f83 | 109 | def fetchguid(self): |
7b99bf75 | 110 | """Fetch user data and posts using guid. |
beaa09fb | 111 | """ |
f50cbea3 MM |
112 | if self['guid']: |
113 | request = self._connection.get('people/{0}.json'.format(self['guid'])) | |
3311aa0c | 114 | self._postproc(request) |
415337b6 | 115 | self._fetchstream() |
3311aa0c MM |
116 | else: |
117 | raise errors.UserError('GUID not set') | |
3cf4514e | 118 | |
5131bd9e MM |
119 | def fetchprofile(self): |
120 | """Fetches user data. | |
121 | """ | |
e21587b9 F |
122 | result = search.Search(self._connection).user(self['handle']) |
123 | ||
124 | # Check if there were any results at all | |
125 | if len(result) < 1: | |
126 | raise errors.UserError('could not fetch profile of user: {0}'.format(self['handle'])) | |
127 | ||
128 | self.data = result[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()] |