53d7ab5751198d54f651908226a3ecd5352f75ce
[diaspy.git] / diaspy / settings.py
1 # -*- coding: utf-8 -*-
2
3 """This module provides access to user's settings on Diaspora*.
4 """
5
6
7 import json
8 import os
9 import urllib
10 import warnings
11
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
20 from diaspy import errors, streams
21
22
23 class Account():
24 """Provides interface to account settings.
25 """
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>')
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')
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
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')
123 if BS4_SUPPORT:
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)
129
130
131 class Privacy():
132 """Provides interface to provacy settings.
133 """
134 def __init__(self, connection):
135 self._connection = connection
136
137
138 class Profile():
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 """
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="(.*?)" />')
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 """
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)
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
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)]
210
211 def getBio(self):
212 """Returns user bio.
213 """
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
221
222 def getLocation(self):
223 """Returns location string.
224 """
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
232
233 def getGender(self):
234 """Returns location string.
235 """
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
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 """
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)
272 else:
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
280 else:
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)
289
290 def isSearchable(self):
291 """Returns True if profile is searchable.
292 """
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
307
308 def isNSFW(self):
309 """Returns True if profile is marked as NSFW.
310 """
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
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)
386 request = self._connection.post('profile', data=self.data, allow_redirects=False)
387 return request.status_code
388
389
390 class Services():
391 """Provides interface to services settings.
392 """
393 def __init__(self, connection):
394 self._connection = connection