##// END OF EJS Templates
Download webm videos from youtube
neko259 -
r1328:3352da82 default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 import os
2 import re
3
4 from django.core.files.uploadedfile import SimpleUploadedFile
5 from pytube import YouTube
6 import requests
7
8 from boards.utils import validate_file_size
9
10 YOUTUBE_VIDEO_FORMAT = 'webm'
11
12 HTTP_RESULT_OK = 200
13
14 HEADER_CONTENT_LENGTH = 'content-length'
15 HEADER_CONTENT_TYPE = 'content-type'
16
17 FILE_DOWNLOAD_CHUNK_BYTES = 100000
18
19 YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+')
20
21
22 class Downloader:
23 @staticmethod
24 def handles(url: str) -> bool:
25 return False
26
27 @staticmethod
28 def download(url: str):
29 # Verify content headers
30 response_head = requests.head(url, verify=False)
31 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
32 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
33 if length_header:
34 length = int(length_header)
35 validate_file_size(length)
36 # Get the actual content into memory
37 response = requests.get(url, verify=False, stream=True)
38
39 # Download file, stop if the size exceeds limit
40 size = 0
41 content = b''
42 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
43 size += len(chunk)
44 validate_file_size(size)
45 content += chunk
46
47 if response.status_code == HTTP_RESULT_OK and content:
48 # Set a dummy file name that will be replaced
49 # anyway, just keep the valid extension
50 filename = 'file.' + content_type.split('/')[1]
51 return SimpleUploadedFile(filename, content, content_type)
52
53
54 class YouTubeDownloader(Downloader):
55 @staticmethod
56 def download(url: str):
57 yt = YouTube()
58 yt.from_url(url)
59 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
60 if len(videos) > 0:
61 video = videos[0]
62 filename = '{}.{}'.format(video.filename, video.extension)
63 try:
64 video.download(on_progress=YouTubeDownloader.on_progress)
65
66 file = open(filename, 'rb')
67 content = file.read()
68 file.close()
69
70 os.remove(filename)
71 return SimpleUploadedFile(filename, content, video.extension)
72 except Exception as e:
73 if os.path.isfile(filename):
74 os.remove(filename)
75 raise e
76
77 @staticmethod
78 def handles(url: str) -> bool:
79 return YOUTUBE_URL.match(url)
80
81 @staticmethod
82 def on_progress(bytes, file_size, start):
83 validate_file_size(file_size)
@@ -1,395 +1,364 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4
4
5 import pytz
5 import pytz
6 from django import forms
6 from django import forms
7 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.exceptions import ObjectDoesNotExist
8 from django.core.exceptions import ObjectDoesNotExist
9 from django.forms.util import ErrorList
9 from django.forms.util import ErrorList
10 from django.utils.translation import ugettext_lazy as _
10 from django.utils.translation import ugettext_lazy as _
11 import requests
12
11
13 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
14 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models import Tag, Post
15 from boards.models import Tag, Post
16 from boards.utils import validate_file_size
16 from neboard import settings
17 from neboard import settings
17 import boards.settings as board_settings
18 import boards.settings as board_settings
18 import neboard
19 import neboard
19
20
20 HEADER_CONTENT_LENGTH = 'content-length'
21 HEADER_CONTENT_TYPE = 'content-type'
22
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24
22
25 VETERAN_POSTING_DELAY = 5
23 VETERAN_POSTING_DELAY = 5
26
24
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 ATTRIBUTE_ROWS = 'rows'
26 ATTRIBUTE_ROWS = 'rows'
29
27
30 LAST_POST_TIME = 'last_post_time'
28 LAST_POST_TIME = 'last_post_time'
31 LAST_LOGIN_TIME = 'last_login_time'
29 LAST_LOGIN_TIME = 'last_login_time'
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34
32
35 LABEL_TITLE = _('Title')
33 LABEL_TITLE = _('Title')
36 LABEL_TEXT = _('Text')
34 LABEL_TEXT = _('Text')
37 LABEL_TAG = _('Tag')
35 LABEL_TAG = _('Tag')
38 LABEL_SEARCH = _('Search')
36 LABEL_SEARCH = _('Search')
39
37
40 ERROR_SPEED = _('Please wait %s seconds before sending message')
38 ERROR_SPEED = _('Please wait %s seconds before sending message')
41
39
42 TAG_MAX_LENGTH = 20
40 TAG_MAX_LENGTH = 20
43
41
44 FILE_DOWNLOAD_CHUNK_BYTES = 100000
45
46 HTTP_RESULT_OK = 200
47
48 TEXTAREA_ROWS = 4
42 TEXTAREA_ROWS = 4
49
43
50
44
51 def get_timezones():
45 def get_timezones():
52 timezones = []
46 timezones = []
53 for tz in pytz.common_timezones:
47 for tz in pytz.common_timezones:
54 timezones.append((tz, tz),)
48 timezones.append((tz, tz),)
55 return timezones
49 return timezones
56
50
57
51
58 class FormatPanel(forms.Textarea):
52 class FormatPanel(forms.Textarea):
59 """
53 """
60 Panel for text formatting. Consists of buttons to add different tags to the
54 Panel for text formatting. Consists of buttons to add different tags to the
61 form text area.
55 form text area.
62 """
56 """
63
57
64 def render(self, name, value, attrs=None):
58 def render(self, name, value, attrs=None):
65 output = '<div id="mark-panel">'
59 output = '<div id="mark-panel">'
66 for formatter in formatters:
60 for formatter in formatters:
67 output += '<span class="mark_btn"' + \
61 output += '<span class="mark_btn"' + \
68 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
62 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
69 '\', \'' + formatter.format_right + '\')">' + \
63 '\', \'' + formatter.format_right + '\')">' + \
70 formatter.preview_left + formatter.name + \
64 formatter.preview_left + formatter.name + \
71 formatter.preview_right + '</span>'
65 formatter.preview_right + '</span>'
72
66
73 output += '</div>'
67 output += '</div>'
74 output += super(FormatPanel, self).render(name, value, attrs=None)
68 output += super(FormatPanel, self).render(name, value, attrs=None)
75
69
76 return output
70 return output
77
71
78
72
79 class PlainErrorList(ErrorList):
73 class PlainErrorList(ErrorList):
80 def __unicode__(self):
74 def __unicode__(self):
81 return self.as_text()
75 return self.as_text()
82
76
83 def as_text(self):
77 def as_text(self):
84 return ''.join(['(!) %s ' % e for e in self])
78 return ''.join(['(!) %s ' % e for e in self])
85
79
86
80
87 class NeboardForm(forms.Form):
81 class NeboardForm(forms.Form):
88 """
82 """
89 Form with neboard-specific formatting.
83 Form with neboard-specific formatting.
90 """
84 """
91
85
92 def as_div(self):
86 def as_div(self):
93 """
87 """
94 Returns this form rendered as HTML <as_div>s.
88 Returns this form rendered as HTML <as_div>s.
95 """
89 """
96
90
97 return self._html_output(
91 return self._html_output(
98 # TODO Do not show hidden rows in the list here
92 # TODO Do not show hidden rows in the list here
99 normal_row='<div class="form-row">'
93 normal_row='<div class="form-row">'
100 '<div class="form-label">'
94 '<div class="form-label">'
101 '%(label)s'
95 '%(label)s'
102 '</div>'
96 '</div>'
103 '<div class="form-input">'
97 '<div class="form-input">'
104 '%(field)s'
98 '%(field)s'
105 '</div>'
99 '</div>'
106 '</div>'
100 '</div>'
107 '<div class="form-row">'
101 '<div class="form-row">'
108 '%(help_text)s'
102 '%(help_text)s'
109 '</div>',
103 '</div>',
110 error_row='<div class="form-row">'
104 error_row='<div class="form-row">'
111 '<div class="form-label"></div>'
105 '<div class="form-label"></div>'
112 '<div class="form-errors">%s</div>'
106 '<div class="form-errors">%s</div>'
113 '</div>',
107 '</div>',
114 row_ender='</div>',
108 row_ender='</div>',
115 help_text_html='%s',
109 help_text_html='%s',
116 errors_on_separate_row=True)
110 errors_on_separate_row=True)
117
111
118 def as_json_errors(self):
112 def as_json_errors(self):
119 errors = []
113 errors = []
120
114
121 for name, field in list(self.fields.items()):
115 for name, field in list(self.fields.items()):
122 if self[name].errors:
116 if self[name].errors:
123 errors.append({
117 errors.append({
124 'field': name,
118 'field': name,
125 'errors': self[name].errors.as_text(),
119 'errors': self[name].errors.as_text(),
126 })
120 })
127
121
128 return errors
122 return errors
129
123
130
124
131 class PostForm(NeboardForm):
125 class PostForm(NeboardForm):
132
126
133 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
127 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
134 label=LABEL_TITLE,
128 label=LABEL_TITLE,
135 widget=forms.TextInput(
129 widget=forms.TextInput(
136 attrs={ATTRIBUTE_PLACEHOLDER:
130 attrs={ATTRIBUTE_PLACEHOLDER:
137 'test#tripcode'}))
131 'test#tripcode'}))
138 text = forms.CharField(
132 text = forms.CharField(
139 widget=FormatPanel(attrs={
133 widget=FormatPanel(attrs={
140 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
134 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
141 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
135 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
142 }),
136 }),
143 required=False, label=LABEL_TEXT)
137 required=False, label=LABEL_TEXT)
144 file = forms.FileField(required=False, label=_('File'),
138 file = forms.FileField(required=False, label=_('File'),
145 widget=forms.ClearableFileInput(
139 widget=forms.ClearableFileInput(
146 attrs={'accept': 'file/*'}))
140 attrs={'accept': 'file/*'}))
147 file_url = forms.CharField(required=False, label=_('File URL'),
141 file_url = forms.CharField(required=False, label=_('File URL'),
148 widget=forms.TextInput(
142 widget=forms.TextInput(
149 attrs={ATTRIBUTE_PLACEHOLDER:
143 attrs={ATTRIBUTE_PLACEHOLDER:
150 'http://example.com/image.png'}))
144 'http://example.com/image.png'}))
151
145
152 # This field is for spam prevention only
146 # This field is for spam prevention only
153 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
147 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
154 widget=forms.TextInput(attrs={
148 widget=forms.TextInput(attrs={
155 'class': 'form-email'}))
149 'class': 'form-email'}))
156 threads = forms.CharField(required=False, label=_('Additional threads'),
150 threads = forms.CharField(required=False, label=_('Additional threads'),
157 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
151 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
158 '123 456 789'}))
152 '123 456 789'}))
159
153
160 session = None
154 session = None
161 need_to_ban = False
155 need_to_ban = False
162
156
163 def clean_title(self):
157 def clean_title(self):
164 title = self.cleaned_data['title']
158 title = self.cleaned_data['title']
165 if title:
159 if title:
166 if len(title) > TITLE_MAX_LENGTH:
160 if len(title) > TITLE_MAX_LENGTH:
167 raise forms.ValidationError(_('Title must have less than %s '
161 raise forms.ValidationError(_('Title must have less than %s '
168 'characters') %
162 'characters') %
169 str(TITLE_MAX_LENGTH))
163 str(TITLE_MAX_LENGTH))
170 return title
164 return title
171
165
172 def clean_text(self):
166 def clean_text(self):
173 text = self.cleaned_data['text'].strip()
167 text = self.cleaned_data['text'].strip()
174 if text:
168 if text:
175 max_length = board_settings.get_int('Forms', 'MaxTextLength')
169 max_length = board_settings.get_int('Forms', 'MaxTextLength')
176 if len(text) > max_length:
170 if len(text) > max_length:
177 raise forms.ValidationError(_('Text must have less than %s '
171 raise forms.ValidationError(_('Text must have less than %s '
178 'characters') % str(max_length))
172 'characters') % str(max_length))
179 return text
173 return text
180
174
181 def clean_file(self):
175 def clean_file(self):
182 file = self.cleaned_data['file']
176 file = self.cleaned_data['file']
183
177
184 if file:
178 if file:
185 self.validate_file_size(file.size)
179 validate_file_size(file.size)
186
180
187 return file
181 return file
188
182
189 def clean_file_url(self):
183 def clean_file_url(self):
190 url = self.cleaned_data['file_url']
184 url = self.cleaned_data['file_url']
191
185
192 file = None
186 file = None
193 if url:
187 if url:
194 file = self._get_file_from_url(url)
188 file = self._get_file_from_url(url)
195
189
196 if not file:
190 if not file:
197 raise forms.ValidationError(_('Invalid URL'))
191 raise forms.ValidationError(_('Invalid URL'))
198 else:
192 else:
199 self.validate_file_size(file.size)
193 validate_file_size(file.size)
200
194
201 return file
195 return file
202
196
203 def clean_threads(self):
197 def clean_threads(self):
204 threads_str = self.cleaned_data['threads']
198 threads_str = self.cleaned_data['threads']
205
199
206 if len(threads_str) > 0:
200 if len(threads_str) > 0:
207 threads_id_list = threads_str.split(' ')
201 threads_id_list = threads_str.split(' ')
208
202
209 threads = list()
203 threads = list()
210
204
211 for thread_id in threads_id_list:
205 for thread_id in threads_id_list:
212 try:
206 try:
213 thread = Post.objects.get(id=int(thread_id))
207 thread = Post.objects.get(id=int(thread_id))
214 if not thread.is_opening() or thread.get_thread().archived:
208 if not thread.is_opening() or thread.get_thread().archived:
215 raise ObjectDoesNotExist()
209 raise ObjectDoesNotExist()
216 threads.append(thread)
210 threads.append(thread)
217 except (ObjectDoesNotExist, ValueError):
211 except (ObjectDoesNotExist, ValueError):
218 raise forms.ValidationError(_('Invalid additional thread list'))
212 raise forms.ValidationError(_('Invalid additional thread list'))
219
213
220 return threads
214 return threads
221
215
222 def clean(self):
216 def clean(self):
223 cleaned_data = super(PostForm, self).clean()
217 cleaned_data = super(PostForm, self).clean()
224
218
225 if cleaned_data['email']:
219 if cleaned_data['email']:
226 self.need_to_ban = True
220 self.need_to_ban = True
227 raise forms.ValidationError('A human cannot enter a hidden field')
221 raise forms.ValidationError('A human cannot enter a hidden field')
228
222
229 if not self.errors:
223 if not self.errors:
230 self._clean_text_file()
224 self._clean_text_file()
231
225
232 if not self.errors and self.session:
226 if not self.errors and self.session:
233 self._validate_posting_speed()
227 self._validate_posting_speed()
234
228
235 return cleaned_data
229 return cleaned_data
236
230
237 def get_file(self):
231 def get_file(self):
238 """
232 """
239 Gets file from form or URL.
233 Gets file from form or URL.
240 """
234 """
241
235
242 file = self.cleaned_data['file']
236 file = self.cleaned_data['file']
243 return file or self.cleaned_data['file_url']
237 return file or self.cleaned_data['file_url']
244
238
245 def get_tripcode(self):
239 def get_tripcode(self):
246 title = self.cleaned_data['title']
240 title = self.cleaned_data['title']
247 if title is not None and '#' in title:
241 if title is not None and '#' in title:
248 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
242 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
249 return hashlib.md5(code.encode()).hexdigest()
243 return hashlib.md5(code.encode()).hexdigest()
250
244
251 def get_title(self):
245 def get_title(self):
252 title = self.cleaned_data['title']
246 title = self.cleaned_data['title']
253 if title is not None and '#' in title:
247 if title is not None and '#' in title:
254 return title.split('#', maxsplit=1)[0]
248 return title.split('#', maxsplit=1)[0]
255 else:
249 else:
256 return title
250 return title
257
251
258 def _clean_text_file(self):
252 def _clean_text_file(self):
259 text = self.cleaned_data.get('text')
253 text = self.cleaned_data.get('text')
260 file = self.get_file()
254 file = self.get_file()
261
255
262 if (not text) and (not file):
256 if (not text) and (not file):
263 error_message = _('Either text or file must be entered.')
257 error_message = _('Either text or file must be entered.')
264 self._errors['text'] = self.error_class([error_message])
258 self._errors['text'] = self.error_class([error_message])
265
259
266 def _validate_posting_speed(self):
260 def _validate_posting_speed(self):
267 can_post = True
261 can_post = True
268
262
269 posting_delay = settings.POSTING_DELAY
263 posting_delay = settings.POSTING_DELAY
270
264
271 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
265 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
272 now = time.time()
266 now = time.time()
273
267
274 current_delay = 0
268 current_delay = 0
275 need_delay = False
269 need_delay = False
276
270
277 if not LAST_POST_TIME in self.session:
271 if not LAST_POST_TIME in self.session:
278 self.session[LAST_POST_TIME] = now
272 self.session[LAST_POST_TIME] = now
279
273
280 need_delay = True
274 need_delay = True
281 else:
275 else:
282 last_post_time = self.session.get(LAST_POST_TIME)
276 last_post_time = self.session.get(LAST_POST_TIME)
283 current_delay = int(now - last_post_time)
277 current_delay = int(now - last_post_time)
284
278
285 need_delay = current_delay < posting_delay
279 need_delay = current_delay < posting_delay
286
280
287 if need_delay:
281 if need_delay:
288 error_message = ERROR_SPEED % str(posting_delay
282 error_message = ERROR_SPEED % str(posting_delay
289 - current_delay)
283 - current_delay)
290 self._errors['text'] = self.error_class([error_message])
284 self._errors['text'] = self.error_class([error_message])
291
285
292 can_post = False
286 can_post = False
293
287
294 if can_post:
288 if can_post:
295 self.session[LAST_POST_TIME] = now
289 self.session[LAST_POST_TIME] = now
296
290
297 def validate_file_size(self, size: int):
298 max_size = board_settings.get_int('Forms', 'MaxFileSize')
299 if size > max_size:
300 raise forms.ValidationError(
301 _('File must be less than %s bytes')
302 % str(max_size))
303
304 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
291 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
305 """
292 """
306 Gets an file file from URL.
293 Gets an file file from URL.
307 """
294 """
308
295
309 img_temp = None
296 img_temp = None
310
297
311 try:
298 try:
312 # Verify content headers
299 for downloader in Downloader.__subclasses__():
313 response_head = requests.head(url, verify=False)
300 if downloader.handles(url):
314 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
301 return downloader.download(url)
315 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
302 # If nobody of the specific downloaders handles this, use generic
316 if length_header:
303 # one
317 length = int(length_header)
304 return Downloader.download(url)
318 self.validate_file_size(length)
305 except forms.ValidationError as e:
319 # Get the actual content into memory
306 raise e
320 response = requests.get(url, verify=False, stream=True)
321
322 # Download file, stop if the size exceeds limit
323 size = 0
324 content = b''
325 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
326 size += len(chunk)
327 self.validate_file_size(size)
328 content += chunk
329
330 if response.status_code == HTTP_RESULT_OK and content:
331 # Set a dummy file name that will be replaced
332 # anyway, just keep the valid extension
333 filename = 'file.' + content_type.split('/')[1]
334 img_temp = SimpleUploadedFile(filename, content,
335 content_type)
336 except Exception as e:
307 except Exception as e:
337 # Just return no file
308 # Just return no file
338 pass
309 pass
339
310
340 return img_temp
341
342
311
343 class ThreadForm(PostForm):
312 class ThreadForm(PostForm):
344
313
345 tags = forms.CharField(
314 tags = forms.CharField(
346 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
315 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
347 max_length=100, label=_('Tags'), required=True)
316 max_length=100, label=_('Tags'), required=True)
348
317
349 def clean_tags(self):
318 def clean_tags(self):
350 tags = self.cleaned_data['tags'].strip()
319 tags = self.cleaned_data['tags'].strip()
351
320
352 if not tags or not REGEX_TAGS.match(tags):
321 if not tags or not REGEX_TAGS.match(tags):
353 raise forms.ValidationError(
322 raise forms.ValidationError(
354 _('Inappropriate characters in tags.'))
323 _('Inappropriate characters in tags.'))
355
324
356 required_tag_exists = False
325 required_tag_exists = False
357 for tag in tags.split():
326 for tag in tags.split():
358 try:
327 try:
359 Tag.objects.get(name=tag.strip().lower(), required=True)
328 Tag.objects.get(name=tag.strip().lower(), required=True)
360 required_tag_exists = True
329 required_tag_exists = True
361 break
330 break
362 except ObjectDoesNotExist:
331 except ObjectDoesNotExist:
363 pass
332 pass
364
333
365 if not required_tag_exists:
334 if not required_tag_exists:
366 all_tags = Tag.objects.filter(required=True)
335 all_tags = Tag.objects.filter(required=True)
367 raise forms.ValidationError(
336 raise forms.ValidationError(
368 _('Need at least one section.'))
337 _('Need at least one section.'))
369
338
370 return tags
339 return tags
371
340
372 def clean(self):
341 def clean(self):
373 cleaned_data = super(ThreadForm, self).clean()
342 cleaned_data = super(ThreadForm, self).clean()
374
343
375 return cleaned_data
344 return cleaned_data
376
345
377
346
378 class SettingsForm(NeboardForm):
347 class SettingsForm(NeboardForm):
379
348
380 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
349 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
381 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
350 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
382 username = forms.CharField(label=_('User name'), required=False)
351 username = forms.CharField(label=_('User name'), required=False)
383 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
352 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
384
353
385 def clean_username(self):
354 def clean_username(self):
386 username = self.cleaned_data['username']
355 username = self.cleaned_data['username']
387
356
388 if username and not REGEX_TAGS.match(username):
357 if username and not REGEX_TAGS.match(username):
389 raise forms.ValidationError(_('Inappropriate characters.'))
358 raise forms.ValidationError(_('Inappropriate characters.'))
390
359
391 return username
360 return username
392
361
393
362
394 class SearchForm(NeboardForm):
363 class SearchForm(NeboardForm):
395 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
364 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,92 +1,103 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 import time
5 import time
6 import hmac
6 import hmac
7
7
8 from django.core.cache import cache
8 from django.core.cache import cache
9 from django.db.models import Model
9 from django.db.models import Model
10 from django import forms
10
11
11 from django.utils import timezone
12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
14 import boards
12
15
13 from neboard import settings
16 from neboard import settings
14
17
15
18
16 CACHE_KEY_DELIMITER = '_'
19 CACHE_KEY_DELIMITER = '_'
17 PERMISSION_MODERATE = 'moderation'
20 PERMISSION_MODERATE = 'moderation'
18
21
19 def get_client_ip(request):
22 def get_client_ip(request):
20 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
23 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
21 if x_forwarded_for:
24 if x_forwarded_for:
22 ip = x_forwarded_for.split(',')[-1].strip()
25 ip = x_forwarded_for.split(',')[-1].strip()
23 else:
26 else:
24 ip = request.META.get('REMOTE_ADDR')
27 ip = request.META.get('REMOTE_ADDR')
25 return ip
28 return ip
26
29
27
30
28 # TODO The output format is not epoch because it includes microseconds
31 # TODO The output format is not epoch because it includes microseconds
29 def datetime_to_epoch(datetime):
32 def datetime_to_epoch(datetime):
30 return int(time.mktime(timezone.localtime(
33 return int(time.mktime(timezone.localtime(
31 datetime,timezone.get_current_timezone()).timetuple())
34 datetime,timezone.get_current_timezone()).timetuple())
32 * 1000000 + datetime.microsecond)
35 * 1000000 + datetime.microsecond)
33
36
34
37
35 def get_websocket_token(user_id='', timestamp=''):
38 def get_websocket_token(user_id='', timestamp=''):
36 """
39 """
37 Create token to validate information provided by new connection.
40 Create token to validate information provided by new connection.
38 """
41 """
39
42
40 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
43 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
41 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
44 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
42 sign.update(user_id.encode())
45 sign.update(user_id.encode())
43 sign.update(timestamp.encode())
46 sign.update(timestamp.encode())
44 token = sign.hexdigest()
47 token = sign.hexdigest()
45
48
46 return token
49 return token
47
50
48
51
49 def cached_result(key_method=None):
52 def cached_result(key_method=None):
50 """
53 """
51 Caches method result in the Django's cache system, persisted by object name,
54 Caches method result in the Django's cache system, persisted by object name,
52 object name and model id if object is a Django model.
55 object name and model id if object is a Django model.
53 """
56 """
54 def _cached_result(function):
57 def _cached_result(function):
55 def inner_func(obj, *args, **kwargs):
58 def inner_func(obj, *args, **kwargs):
56 # TODO Include method arguments to the cache key
59 # TODO Include method arguments to the cache key
57 cache_key_params = [obj.__class__.__name__, function.__name__]
60 cache_key_params = [obj.__class__.__name__, function.__name__]
58 if isinstance(obj, Model):
61 if isinstance(obj, Model):
59 cache_key_params.append(str(obj.id))
62 cache_key_params.append(str(obj.id))
60
63
61 if key_method is not None:
64 if key_method is not None:
62 cache_key_params += [str(arg) for arg in key_method(obj)]
65 cache_key_params += [str(arg) for arg in key_method(obj)]
63
66
64 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
67 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
65
68
66 persisted_result = cache.get(cache_key)
69 persisted_result = cache.get(cache_key)
67 if persisted_result is not None:
70 if persisted_result is not None:
68 result = persisted_result
71 result = persisted_result
69 else:
72 else:
70 result = function(obj, *args, **kwargs)
73 result = function(obj, *args, **kwargs)
71 cache.set(cache_key, result)
74 cache.set(cache_key, result)
72
75
73 return result
76 return result
74
77
75 return inner_func
78 return inner_func
76 return _cached_result
79 return _cached_result
77
80
78
81
79 def is_moderator(request):
82 def is_moderator(request):
80 try:
83 try:
81 moderate = request.user.has_perm(PERMISSION_MODERATE)
84 moderate = request.user.has_perm(PERMISSION_MODERATE)
82 except AttributeError:
85 except AttributeError:
83 moderate = False
86 moderate = False
84
87
85 return moderate
88 return moderate
86
89
87
90
88 def get_file_hash(file) -> str:
91 def get_file_hash(file) -> str:
89 md5 = hashlib.md5()
92 md5 = hashlib.md5()
90 for chunk in file.chunks():
93 for chunk in file.chunks():
91 md5.update(chunk)
94 md5.update(chunk)
92 return md5.hexdigest()
95 return md5.hexdigest()
96
97
98 def validate_file_size(size: int):
99 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
100 if size > max_size:
101 raise forms.ValidationError(
102 _('File must be less than %s bytes')
103 % str(max_size))
@@ -1,8 +1,9 b''
1 pytube
1 requests
2 requests
2 adjacent
3 adjacent
3 django-haystack
4 django-haystack
4 pillow
5 pillow
5 django>=1.8
6 django>=1.8
6 bbcode
7 bbcode
7 django-debug-toolbar
8 django-debug-toolbar
8 pytz
9 pytz
General Comments 0
You need to be logged in to leave comments. Login now