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