Commit | Line | Data |
---|---|---|
e2c48b2f MM |
1 | # -*- coding: utf-8 -*- |
2 | ||
dea56a86 MM |
3 | """This module provides access to user's settings on Diaspora*. |
4 | """ | |
5 | ||
6 | ||
fb680551 | 7 | import json |
f61c14c1 | 8 | import os |
fb680551 | 9 | import urllib |
f61c14c1 | 10 | import warnings |
fb680551 | 11 | |
343904a9 C |
12 | BS4_SUPPORT=False |
13 | try: | |
14 | from bs4 import BeautifulSoup | |
15 | except ImportError: | |
16 | import re | |
17 | print("[diaspy] BeautifulSoup not found, falling back on regex.") | |
18 | else: BS4_SUPPORT=True | |
19 | ||
f61c14c1 | 20 | from diaspy import errors, streams |
0d5880af | 21 | |
fb680551 | 22 | |
3def6e18 | 23 | class Account(): |
d95ff94a C |
24 | """Provides interface to account settings. |
25 | """ | |
343904a9 C |
26 | if not BS4_SUPPORT: |
27 | email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"') | |
28 | language_option_regexp = re.compile('<option value="([_a-zA-Z-]+)"(?: selected="selected")?>(.*?)</option>') | |
d95ff94a C |
29 | |
30 | def __init__(self, connection): | |
31 | self._connection = connection | |
32 | ||
33 | def downloadxml(self): | |
34 | """Returns downloaded XML. | |
35 | """ | |
36 | request = self._connection.get('user/export') | |
37 | return request.text | |
38 | ||
39 | def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None): | |
40 | """Downloads photos into the current working directory. | |
41 | Sizes are: large, medium, small. | |
42 | Filename is: {post_guid}_{photo_guid}.{extension} | |
43 | ||
44 | Normally, this method will catch urllib-generated errors and | |
45 | just issue warnings about photos that couldn't be downloaded. | |
46 | However, with _critical param set to True errors will become | |
47 | critical - the will be reraised in finally block. | |
48 | ||
49 | :param size: size of the photos to download - large, medium or small | |
50 | :type size: str | |
51 | :param path: path to download (defaults to current working directory | |
52 | :type path: str | |
53 | :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw, | |
54 | :type mark_nsfw: bool | |
55 | :param _stream: diaspy.streams.Generic-like object (only for testing) | |
56 | :param _critical: if True urllib errors will be reraised after generating a warning (may be removed) | |
57 | ||
58 | :returns: integer, number of photos downloaded | |
59 | """ | |
60 | photos = 0 | |
61 | if _stream is None: | |
62 | stream = streams.Activity(self._connection) | |
63 | stream.full() | |
64 | else: | |
65 | stream = _stream | |
66 | for i, post in enumerate(stream): | |
67 | if post['nsfw'] is not False: nsfw = '-nsfw' | |
68 | else: nsfw = '' | |
69 | if post['photos']: | |
70 | for n, photo in enumerate(post['photos']): | |
71 | # photo format -- .jpg, .png etc. | |
72 | ext = photo['sizes'][size].split('.')[-1] | |
73 | name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext) | |
74 | filename = os.path.join(path, name) | |
75 | try: | |
76 | urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename) | |
77 | except (urllib.error.HTTPError, urllib.error.URLError) as e: | |
78 | warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e)) | |
79 | finally: | |
80 | if _critical: raise | |
81 | photos += 1 | |
82 | return photos | |
83 | ||
84 | def setEmail(self, email): | |
85 | """Changes user's email. | |
86 | """ | |
87 | data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)} | |
88 | request = self._connection.post('user', data=data, allow_redirects=False) | |
89 | if request.status_code != 302: | |
90 | raise errors.SettingsError('setting email failed: {0}'.format(request.status_code)) | |
91 | ||
92 | def getEmail(self): | |
93 | """Returns currently used email. | |
94 | """ | |
95 | data = self._connection.get('user/edit') | |
343904a9 C |
96 | if BS4_SUPPORT: |
97 | soup = BeautifulSoup(data.text, 'lxml') | |
98 | email = soup.find('input', {"id": "user_email"}) | |
99 | if email: email = email['value'] | |
100 | else: email = '' | |
101 | return email | |
102 | else: | |
103 | email = self.email_regexp.search(data.text) | |
104 | if email is None: email = '' | |
105 | else: email = email.group(1) | |
106 | return email | |
d95ff94a C |
107 | |
108 | def setLanguage(self, lang): | |
109 | """Changes user's email. | |
110 | ||
111 | :param lang: language identifier from getLanguages() | |
112 | """ | |
113 | data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)} | |
114 | request = self._connection.post('user', data=data, allow_redirects=False) | |
115 | if request.status_code != 302: | |
116 | raise errors.SettingsError('setting language failed: {0}'.format(request.status_code)) | |
117 | ||
118 | def getLanguages(self): | |
119 | """Returns a list of tuples containing ('Language name', 'identifier'). | |
120 | One of the Black Magic(tm) methods. | |
121 | """ | |
122 | request = self._connection.get('user/edit') | |
f54aff84 | 123 | if BS4_SUPPORT: |
343904a9 C |
124 | soup = BeautifulSoup(request.text, 'lxml') |
125 | language = soup.find('select', {"id": "user_language"}) | |
126 | return [(option.text, option['value']) for option in language.findAll('option')] | |
127 | else: | |
128 | return self.language_option_regexp.findall(request.text) | |
3def6e18 MM |
129 | |
130 | ||
131 | class Privacy(): | |
d95ff94a C |
132 | """Provides interface to provacy settings. |
133 | """ | |
134 | def __init__(self, connection): | |
135 | self._connection = connection | |
3def6e18 MM |
136 | |
137 | ||
36f274c0 | 138 | class Profile(): |
d95ff94a C |
139 | """Provides interface to profile settigns. |
140 | ||
141 | WARNING: | |
142 | ||
143 | Because of the way update requests for profile are created every field must be sent. | |
144 | The `load()` method is used to load all information into the dictionary. | |
145 | Setters can then be used to adjust the data. | |
146 | Finally, `update()` can be called to send data back to pod. | |
147 | """ | |
343904a9 C |
148 | if not BS4_SUPPORT: |
149 | firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />') | |
150 | lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />') | |
151 | bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>') | |
152 | location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />') | |
153 | gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />') | |
154 | birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>') | |
155 | birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>') | |
156 | birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>') | |
157 | is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />') | |
158 | is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />') | |
d95ff94a C |
159 | |
160 | def __init__(self, connection, no_load=False): | |
161 | self._connection = connection | |
162 | self.data = {'utf-8': '✓', | |
163 | '_method': 'put', | |
164 | 'profile[first_name]': '', | |
165 | 'profile[last_name]': '', | |
166 | 'profile[tag_string]': '', | |
167 | 'tags': '', | |
168 | 'file': '', | |
169 | 'profile[bio]': '', | |
170 | 'profile[location]': '', | |
171 | 'profile[gender]': '', | |
172 | 'profile[date][year]': '', | |
173 | 'profile[date][month]': '', | |
174 | 'profile[date][day]': '', | |
175 | } | |
176 | self._html = self._fetchhtml() | |
177 | self._loaded = False | |
178 | if not no_load: self.load() | |
179 | ||
180 | def _fetchhtml(self): | |
181 | """Fetches html that will be used to extract data. | |
182 | """ | |
183 | return self._connection.get('profile/edit').text | |
184 | ||
185 | def getName(self): | |
186 | """Returns two-tuple: (first, last) name. | |
187 | """ | |
343904a9 C |
188 | if BS4_SUPPORT: |
189 | soup = BeautifulSoup(self._html, 'lxml') | |
190 | first = soup.find('input', {"id": "profile_first_name"}) | |
191 | last = soup.find('input', {"id": "profile_last_name"}) | |
192 | return (first['value'], last['value']) | |
193 | else: | |
194 | first = self.firstname_regexp.search(self._html).group(1) | |
195 | last = self.lastname_regexp.search(self._html).group(1) | |
196 | return (first, last) | |
d95ff94a C |
197 | |
198 | def getTags(self): | |
199 | """Returns tags user had selected when describing him/her-self. | |
200 | """ | |
201 | guid = self._connection.getUserData()['guid'] | |
202 | html = self._connection.get('people/{0}'.format(guid)).text | |
343904a9 C |
203 | if BS4_SUPPORT: |
204 | soup = BeautifulSoup(html, 'lxml') | |
205 | tags = soup.find('meta', {"name": "keywords"}) | |
206 | return [tag.lower() for tag in tags['content'].split(", ")] | |
207 | else: | |
208 | description_regexp = re.compile('<a href="/tags/(.*?)" class="tag">#.*?</a>') | |
209 | return [tag.lower() for tag in re.findall(description_regexp, html)] | |
d95ff94a C |
210 | |
211 | def getBio(self): | |
212 | """Returns user bio. | |
213 | """ | |
343904a9 C |
214 | if BS4_SUPPORT: |
215 | soup = BeautifulSoup(self._html, 'lxml') | |
216 | bio = soup.find('textarea', {"id": "profile_bio"}) | |
217 | return bio.get_text() | |
218 | else: | |
219 | bio = self.bio_regexp.search(self._html).group(1) | |
220 | return bio | |
d95ff94a C |
221 | |
222 | def getLocation(self): | |
223 | """Returns location string. | |
224 | """ | |
343904a9 C |
225 | if BS4_SUPPORT: |
226 | soup = BeautifulSoup(self._html, 'lxml') | |
227 | location = soup.find('input', {"id": "profile_location"}) | |
228 | return location['value'] | |
229 | else: | |
230 | location = self.location_regexp.search(self._html).group(1) | |
231 | return location | |
d95ff94a C |
232 | |
233 | def getGender(self): | |
234 | """Returns location string. | |
235 | """ | |
343904a9 C |
236 | if BS4_SUPPORT: |
237 | soup = BeautifulSoup(self._html, 'lxml') | |
238 | gender = soup.find('input', {"id": "profile_gender"}) | |
239 | return gender['value'] | |
240 | else: | |
241 | gender = self.gender_regexp.search(self._html).group(1) | |
242 | return gender | |
d95ff94a C |
243 | |
244 | def getBirthDate(self, named_month=False): | |
245 | """Returns three-tuple: (year, month, day). | |
246 | ||
247 | :param named_month: if True, return name of the month instead of integer | |
248 | :type named_month: bool | |
249 | """ | |
343904a9 C |
250 | if BS4_SUPPORT: |
251 | soup = BeautifulSoup(self._html, 'lxml') | |
252 | ||
253 | year = soup.find('select', {"id": "profile_date_year"}) | |
254 | year_option = year.find('option', selected=True) | |
255 | if year_option is None: year_option = -1 | |
256 | else: year_option = int(year_option['value']) | |
257 | ||
258 | month = soup.find('select', {"id": "profile_date_month"}) | |
259 | month_option = month.find('option', selected=True) | |
260 | if month_option is None: | |
261 | if named_month: month_option = '' | |
262 | else: month_option = -1 | |
263 | elif named_month: | |
264 | month_option = month_option.text | |
265 | else: month_option = month_option['value'] | |
266 | ||
267 | day = soup.find('select', {"id": "profile_date_day"}) | |
268 | day_option = day.find('option', selected=True) | |
269 | if day_option is None: day_option = -1 | |
270 | else: day_option = int(day_option['value']) | |
271 | return (year_option, month_option, day_option) | |
d95ff94a | 272 | else: |
343904a9 C |
273 | year = self.birth_year_regexp.search(self._html) |
274 | if year is None: year = -1 | |
275 | else: year = int(year.group(1)) | |
276 | month = self.birth_month_regexp.search(self._html) | |
277 | if month is None: | |
278 | if named_month: month = '' | |
279 | else: month = -1 | |
d95ff94a | 280 | else: |
343904a9 C |
281 | if named_month: |
282 | month = month.group(2) | |
283 | else: | |
284 | month = int(month.group(1)) | |
285 | day = self.birth_day_regexp.search(self._html) | |
286 | if day is None: day = -1 | |
287 | else: day = int(day.group(1)) | |
288 | return (year, month, day) | |
d95ff94a C |
289 | |
290 | def isSearchable(self): | |
291 | """Returns True if profile is searchable. | |
292 | """ | |
343904a9 C |
293 | if BS4_SUPPORT: |
294 | soup = BeautifulSoup(self._html, 'lxml') | |
295 | searchable = soup.find('input', {"id": "profile_searchable"}) | |
296 | if (searchable.has_attr('checked') and | |
297 | searchable['checked'] == 'checked'): | |
298 | return True | |
299 | else: return False | |
300 | else: | |
301 | searchable = self.is_searchable_regexp.search(self._html) | |
302 | # this is because value="true" in every case so we just | |
303 | # check if the field is "checked" | |
304 | if searchable is None: searchable = False # if it isn't - the regexp just won't match | |
305 | else: searchable = True | |
306 | return searchable | |
d95ff94a C |
307 | |
308 | def isNSFW(self): | |
309 | """Returns True if profile is marked as NSFW. | |
310 | """ | |
343904a9 C |
311 | if BS4_SUPPORT: |
312 | soup = BeautifulSoup(self._html, 'lxml') | |
313 | nsfw = soup.find('input', {"id": "profile_nsfw"}) | |
314 | if (nsfw.has_attr('checked') and | |
315 | nsfw['checked'] == 'checked'): | |
316 | return True | |
317 | else: return False | |
318 | else: | |
319 | nsfw = self.is_nsfw_regexp.search(self._html) | |
320 | if nsfw is None: nsfw = False | |
321 | else: nsfw = True | |
322 | return nsfw | |
d95ff94a C |
323 | |
324 | def setName(self, first, last): | |
325 | """Set first and last name. | |
326 | """ | |
327 | self.data['profile[first_name]'] = first | |
328 | self.data['profile[last_name]'] = last | |
329 | ||
330 | def setTags(self, tags): | |
331 | """Sets tags that describe the user. | |
332 | """ | |
333 | self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags]) | |
334 | ||
335 | def setBio(self, bio): | |
336 | """Set bio of a user. | |
337 | """ | |
338 | self.data['profile[bio]'] = bio | |
339 | ||
340 | def setLocation(self, location): | |
341 | """Set location of a user. | |
342 | """ | |
343 | self.data['profile[location]'] = location | |
344 | ||
345 | def setGender(self, gender): | |
346 | """Set gender of a user. | |
347 | """ | |
348 | self.data['profile[gender]'] = gender | |
349 | ||
350 | def setBirthDate(self, year, month, day): | |
351 | """Set birth date of a user. | |
352 | """ | |
353 | self.data['profile[date][year]'] = year | |
354 | self.data['profile[date][month]'] = month | |
355 | self.data['profile[date][day]'] = day | |
356 | ||
357 | def setSearchable(self, searchable): | |
358 | """Set user's searchable status. | |
359 | """ | |
360 | self.data['profile[searchable]'] = json.dumps(searchable) | |
361 | ||
362 | def setNSFW(self, nsfw): | |
363 | """Set user NSFW status. | |
364 | """ | |
365 | self.data['profile[nsfw]'] = json.dumps(nsfw) | |
366 | ||
367 | def load(self): | |
368 | """Loads profile data into self.data dictionary. | |
369 | **Notice:** Not all keys are loaded yet. | |
370 | """ | |
371 | self.setName(*self.getName()) | |
372 | self.setBio(self.getBio()) | |
373 | self.setLocation(self.getLocation()) | |
374 | self.setGender(self.getGender()) | |
375 | self.setBirthDate(*self.getBirthDate(named_month=False)) | |
376 | self.setSearchable(self.isSearchable()) | |
377 | self.setNSFW(self.isNSFW()) | |
378 | self.setTags(self.getTags()) | |
379 | self._loaded = True | |
380 | ||
381 | def update(self): | |
382 | """Updates profile information. | |
383 | """ | |
384 | if not self._loaded: raise errors.DiaspyError('profile was not loaded') | |
385 | self.data['authenticity_token'] = repr(self._connection) | |
d95ff94a C |
386 | request = self._connection.post('profile', data=self.data, allow_redirects=False) |
387 | return request.status_code | |
b7a15036 | 388 | |
3def6e18 MM |
389 | |
390 | class Services(): | |
d95ff94a C |
391 | """Provides interface to services settings. |
392 | """ | |
393 | def __init__(self, connection): | |
394 | self._connection = connection |