Completed
Push — master ( 51d889...c8777d )
by Andrew
29s
created

up_files_to_img()   A

Complexity

Conditions 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 13
rs 9.4285
1
import sys
2
3
import base64
4
import datetime
5
import json
6
import logging
7
import re
8
import requests
9
from django.conf import settings
10
from django.contrib.auth import get_user_model
11
from django.core.exceptions import ValidationError
12
from django.core.files.uploadedfile import InMemoryUploadedFile
13
from django.core.mail import send_mail
14
from django.core.validators import validate_email
15
from django.db import IntegrityError
16
from django.db import connection, OperationalError, InterfaceError
17
from django.template import RequestContext
18
from django.template.loader import render_to_string
19
from django.utils.safestring import mark_safe
20
from django.utils.timezone import utc
21
from io import BytesIO
22
23
from chat import local
24
from chat.models import Image, UploadedFile
25
from chat.models import Room
26
from chat.models import User
27
from chat.models import UserProfile, Verification, RoomUsers, IpAddress
28
from chat.py2_3 import urlopen, dict_values_to_list
29
from chat.tornado.constants import RedisPrefix
30
from chat.tornado.constants import VarNames
31
from chat.tornado.message_creator import MessagesCreator
32
33
USERNAME_REGEX = str(settings.MAX_USERNAME_LENGTH).join(['^[a-zA-Z-_0-9]{1,', '}$'])
34
35
logger = logging.getLogger(__name__)
36
37
38
def is_blank(check_str):
39
	if check_str and check_str.strip():
40
		return False
41
	else:
42
		return True
43
44
45
def get_users_in_current_user_rooms(user_id):
46
	user_rooms = Room.objects.filter(users__id=user_id, disabled=False).values('id', 'name', 'roomusers__notifications', 'roomusers__volume')
47
	res = {room['id']: {
48
			VarNames.ROOM_NAME: room['name'],
49
			VarNames.NOTIFICATIONS: room['roomusers__notifications'],
50
			VarNames.VOLUME: room['roomusers__volume'],
51
			VarNames.ROOM_USERS: {}
52
		} for room in user_rooms}
53
	room_ids = (room_id for room_id in res)
54
	rooms_users = User.objects.filter(rooms__in=room_ids).values('id', 'username', 'sex', 'rooms__id')
55
	for user in rooms_users:
56
		dict = res[user['rooms__id']][VarNames.ROOM_USERS]
57
		dict[user['id']] = RedisPrefix.set_js_user_structure(user['username'], user['sex'])
58
	return res
59
60
61
def get_or_create_room(channels, room_id, user_id):
62
	if room_id not in channels:
63
		raise ValidationError("Access denied, only allowed for channels {}".format(channels))
64
	room = do_db(Room.objects.get, id=room_id)
65
	if room.is_private:
66
		raise ValidationError("You can't add users to direct room, create a new room instead")
67
	try:
68
		Room.users.through.objects.create(room_id=room_id, user_id=user_id)
69
	except IntegrityError:
70
		raise ValidationError("User is already in channel")
71
	return room
72
73
74
def update_room(room_id, disabled):
75
	if not disabled:
76
		raise ValidationError('This room already exist')
77
	else:
78
		Room.objects.filter(id=room_id).update(disabled=False)
79
80
81
def create_room(self_user_id, user_id):
82
	# get all self private rooms ids
83
	user_rooms = evaluate(Room.users.through.objects.filter(user_id=self_user_id, room__name__isnull=True).values('room_id'))
84
	# get private room that contains another user from rooms above
85
	if user_rooms and self_user_id == user_id:
86
		room_id = create_self_room(self_user_id,user_rooms)
87
	elif user_rooms:
88
		room_id = create_other_room(self_user_id, user_id, user_rooms)
89
	else:
90
		room_id = create_other_room_wo_check(self_user_id, user_id)
91
	return room_id
92
93
94
def create_other_room_wo_check(self_user_id, user_id):
95
	room = Room()
96
	room.save()
97
	room_id = room.id
98
	if self_user_id == user_id:
99
		RoomUsers(user_id=user_id, room_id=room_id).save()
100
	else:
101
		RoomUsers.objects.bulk_create([
102
			RoomUsers(user_id=self_user_id, room_id=room_id),
103
			RoomUsers(user_id=user_id, room_id=room_id),
104
		])
105
	return room_id
106
107
108
def create_other_room(self_user_id, user_id, user_rooms):
109
	rooms_query = RoomUsers.objects.filter(user_id=user_id, room__in=user_rooms)
110
	query = rooms_query.values('room__id', 'room__disabled')
111
	try:
112
		room = do_db(query.get)
113
		room_id = room['room__id']
114
		update_room(room_id, room['room__disabled'])
115
	except RoomUsers.DoesNotExist:
116
		room = Room()
117
		room.save()
118
		room_id = room.id
119
		RoomUsers.objects.bulk_create([
120
			RoomUsers(user_id=self_user_id, room_id=room_id),
121
			RoomUsers(user_id=user_id, room_id=room_id),
122
		])
123
	return room_id
124
125
126
def evaluate(query_set):
127
	do_db(len, query_set)
128
	return query_set
129
130
131
def create_self_room(self_user_id, user_rooms):
132
	room_ids = list([room['room_id'] for room in evaluate(user_rooms)])
133
	query = execute_query(settings.SELECT_SELF_ROOM, [room_ids, ])
134
	if query:
135
		room_id = query[0]['room__id']
136
		update_room(room_id, query[0]['room__disabled'])
137
	else:
138
		room = Room()
139
		room.save()
140
		room_id = room.id
141
		RoomUsers(user_id=self_user_id, room_id=room_id, notifications=False).save()
142
	return room_id
143
144
def validate_edit_message(self_id, message):
145
	if message.sender_id != self_id:
146
		raise ValidationError("You can only edit your messages")
147
	if message.deleted:
148
		raise ValidationError("Already deleted")
149
150
151
def do_db(callback, *args, **kwargs):
152
	try:
153
		return callback(*args, **kwargs)
154
	except (OperationalError, InterfaceError) as e:
155
		if 'MySQL server has gone away' in str(e):
156
			logger.warning('%s, reconnecting' % e)
157
			connection.close()
158
			return callback(*args, **kwargs)
159
		else:
160
			raise e
161
162
163
def execute_query(query, *args, **kwargs):
164
	cursor = connection.cursor()
165
	cursor.execute(query, *args, **kwargs)
166
	desc = cursor.description
167
	return [
168
		dict(zip([col[0] for col in desc], row))
169
		for row in cursor.fetchall()
170
	]
171
172
173
def hide_fields(post, fields, huge=False, fill_with='****'):
174
	"""
175
	:param post: Object that will be copied
176
	:type post: QueryDict
177
	:param fields: fields that will be removed
178
	:param fill_with: replace characters for hidden fields
179
	:param huge: if true object will be cloned and then fields will be removed
180
	:return: a shallow copy of dictionary without specified fields
181
	"""
182
	if not huge:
183
		# hide *fields in shallow copy
184
		res = post.copy()
185
		for field in fields:
186
			if field in post:  # check if field was absent
187
				res[field] = fill_with
188
	else:
189
		# copy everything but *fields
190
		res = {}
191
		for field in post:
192
			# _______________________if this is field to remove
193
			res[field] = post[field] if field not in fields else fill_with
194
	return res
195
196
197
def check_password(password):
198
	"""
199
	Checks if password is secure
200
	:raises ValidationError exception if password is not valid
201
	"""
202
	if is_blank(password):
203
		raise ValidationError("password can't be empty")
204
	if not re.match(u'^\S.+\S$', password):
205
		raise ValidationError("password should be at least 3 symbols")
206
207
208
def check_email(email):
209
	"""
210
	:raises ValidationError if specified email is registered or not valid
211
	"""
212
	if not email:
213
		return
214
	try:
215
		validate_email(email)
216
		# theoretically can throw returning 'more than 1' error
217
		UserProfile.objects.get(email=email)
218
		raise ValidationError('Email {} is already used'.format(email))
219
	except User.DoesNotExist:
220
		pass
221
222
223
def check_user(username):
224
	"""
225
	Checks if specified username is free to register
226
	:type username str
227
	:raises ValidationError exception if username is not valid
228
	"""
229
	# Skip javascript validation, only summary message
230
	validate_user(username)
231
	try:
232
		# theoretically can throw returning 'more than 1' error
233
		User.objects.get(username=username)
234
		raise ValidationError("Username {} is already used. Please select another one".format(username))
235
	except User.DoesNotExist:
236
		pass
237
238
239
def validate_user(username):
240
	if is_blank(username):
241
		raise ValidationError("Username can't be empty")
242
	if not re.match(USERNAME_REGEX, username):
243
		raise ValidationError("Username {} doesn't match regex {}".format(username, USERNAME_REGEX))
244
245
246
def get_client_ip(request):
247
	x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
248
	return x_forwarded_for.split(',')[-1].strip() if x_forwarded_for else request.META.get('REMOTE_ADDR')
249
250
251
def check_captcha(request):
252
	"""
253
	:type request: WSGIRequest
254
	:raises ValidationError: if captcha is not valid or not set
255
	If RECAPTCHA_SECRET_KEY is enabled in settings validates request with it
256
	"""
257
	if not hasattr(settings, 'RECAPTCHA_SECRET_KEY'):
258
		logger.debug('Skipping captcha validation')
259
		return
260
	try:
261
		captcha_rs = request.POST.get('g-recaptcha-response')
262
		url = "https://www.google.com/recaptcha/api/siteverify"
263
		params = {
264
			'secret': settings.RECAPTCHA_SECRET_KEY,
265
			'response': captcha_rs,
266
			'remoteip': local.client_ip
267
		}
268
		raw_response = requests.post(url, params=params, verify=True)
269
		response = raw_response.json()
270
		if not response.get('success', False):
271
			logger.debug('Captcha is NOT valid, response: %s', raw_response)
272
			raise ValidationError(
273
				response['error-codes'] if response.get('error-codes', None) else 'This captcha already used')
274
		logger.debug('Captcha is valid, response: %s', raw_response)
275
	except Exception as e:
276
		raise ValidationError('Unable to check captcha because {}'.format(e))
277
278
279
def send_sign_up_email(user, site_address, request):
280
	if user.email is not None:
281
		verification = Verification(user=user, type_enum=Verification.TypeChoices.register)
282
		verification.save()
283
		user.email_verification = verification
284
		user.save(update_fields=['email_verification'])
285
		link = "{}://{}/confirm_email?token={}".format(settings.SITE_PROTOCOL, site_address, verification.token)
286
		text = ('Hi {}, you have registered pychat'
287
				  '\nTo complete your registration please click on the url bellow: {}'
288
				  '\n\nIf you find any bugs or propositions you can post them {}').format(
289
			user.username, link, settings.ISSUES_REPORT_LINK)
290
		start_message = mark_safe((
291
			"You have registered in <b>Pychat</b>. If you find any bugs or propositions you can post them"
292
			" <a href='{}'>here</a>. To complete your registration please click on the link below.").format(
293
				settings.ISSUES_REPORT_LINK))
294
		context = {
295
			'username': user.username,
296
			'magicLink': link,
297
			'btnText': "CONFIRM SIGN UP",
298
			'greetings': start_message
299
		}
300
		html_message = render_to_string('sign_up_email.html', context, context_instance=RequestContext(request))
301
		logger.info('Sending verification email to userId %s (email %s)', user.id, user.email)
302
		try:
303
			send_mail("Confirm chat registration", text, site_address, [user.email, ], html_message=html_message, fail_silently=True)
304
		except Exception as e:
305
			logging.error("Failed to send registration email because {}".format(e))
306
		else:
307
			logger.info('Email %s has been sent', user.email)
308
309
310
def send_reset_password_email(request, user_profile, verification):
311
	link = "{}://{}/restore_password?token={}".format(settings.SITE_PROTOCOL, request.get_host(), verification.token)
312
	message = "{},\n" \
313
				 "You requested to change a password on site {}.\n" \
314
				 "To proceed click on the link {}\n" \
315
				 "If you didn't request the password change just ignore this mail" \
316
		.format(user_profile.username, request.get_host(), link)
317
	ip_info = get_or_create_ip(get_client_ip(request), logger)
318
	start_message = mark_safe(
319
		"You have requested to send you a magic link to quickly restore password to <b>Pychat</b>. "
320
		"If it wasn't you, you can safely ignore this email")
321
	context = {
322
		'username': user_profile.username,
323
		'magicLink': link,
324
		'ipInfo': ip_info.info,
325
		'ip': ip_info.ip,
326
		'btnText': "CHANGE PASSWORD",
327
		'timeCreated': verification.time,
328
		'greetings': start_message
329
	}
330
	html_message = render_to_string('token_email.html', context, context_instance=RequestContext(request))
331
	send_mail("Pychat: restore password", message, request.get_host(), (user_profile.email,), fail_silently=False,
332
				 html_message=html_message)
333
334
335
def get_user_by_code(token, type):
336
	"""
337
	:param token: token code to verify
338
	:type token: str
339
	:raises ValidationError: if token is not usable
340
	:return: UserProfile, Verification: if token is usable
341
	"""
342
	try:
343
		v = Verification.objects.get(token=token)
344
		if v.type_enum != type:
345
			raise ValidationError("it's not for this operation ")
346
		if v.verified:
347
			raise ValidationError("it's already used")
348
		# TODO move to sql query or leave here?
349
		if v.time < datetime.datetime.utcnow().replace(tzinfo=utc) - datetime.timedelta(days=1):
350
			raise ValidationError("it's expired")
351
		return UserProfile.objects.get(id=v.user_id), v
352
	except Verification.DoesNotExist:
353
		raise ValidationError('Unknown verification token')
354
355
356
def send_new_email_ver(request, user, email):
357
	new_ver = Verification(user=user, type_enum=Verification.TypeChoices.confirm_email, email=email)
358
	new_ver.save()
359
	link = "{}://{}/confirm_email?token={}".format(settings.SITE_PROTOCOL, request.get_host(), new_ver.token)
360
	text = ('Hi {}, you have changed email to curren on pychat \nTo verify it, please click on the url: {}') \
361
		.format(user.username, link)
362
	start_message = mark_safe("You have changed email to current one in  <b>Pychat</b>. \n"
363
									  "To stay safe please verify it by clicking on the url below.")
364
	context = {
365
		'username': user.username,
366
		'magicLink': link,
367
		'btnText': "CONFIRM THIS EMAIL",
368
		'greetings': start_message
369
	}
370
	html_message = render_to_string('sign_up_email.html', context, context_instance=RequestContext(request))
371
	logger.info('Sending verification email to userId %s (email %s)', user.id, email)
372
	try:
373
		send_mail("Confirm this email", text, request.get_host(), [email, ], html_message=html_message,
374
				 fail_silently=False)
375
		return new_ver
376
	except Exception as e:
377
		logger.exception("Failed to send email")
378 View Code Duplication
		raise ValidationError(e.message)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
379
380
381
def send_email_change(request, username, old_email, verification, new_email):
382
	link = "{}://{}/change_email?token={}".format(settings.SITE_PROTOCOL, request.get_host(), verification.token)
383
	message = "{},\n" \
384
				 "You requested to change an email from {} to {} on site {}.\n" \
385
				 "To proceed click on the link {}\n" \
386
				 "If you didn't request the email change someone has hijacked your account. Please change your password" \
387
		.format(old_email, new_email, username, request.get_host(), link)
388
	ip_info = get_or_create_ip(get_client_ip(request), logger)
389
	start_message = mark_safe(
390
		"You have requested an email change on <b>Pychat</b>. After you click on the url bellow we replace email in your profile from current one ({}) to {}. If it wasn't you please change your password as soon as possible".format(old_email, new_email))
391
	context = {
392
		'username': username,
393
		'magicLink': link,
394
		'ipInfo': ip_info.info,
395
		'ip': ip_info.ip,
396
		'btnText': "CHANGE EMAIL",
397
		'timeCreated': verification.time,
398
		'greetings': start_message
399
	}
400
	html_message = render_to_string('token_email.html', context, context_instance=RequestContext(request))
401
	send_mail("Pychat: change email", message, request.get_host(), (old_email,), fail_silently=False,
402
				 html_message=html_message)
403
404
405
def send_password_changed(request, email):
406
	message = "Password has been changed for user {}".format(request.user.username)
407
	ip_info = get_or_create_ip(get_client_ip(request), logger)
408
	context = {
409
		'username': request.user.username,
410
		'ipInfo': ip_info.info,
411
		'ip': ip_info.ip,
412
		'timeCreated': datetime.datetime.now(),
413
	}
414
	html_message = render_to_string('change_password.html', context, context_instance=RequestContext(request))
415
	send_mail("Pychat: password change", message, request.get_host(), (email,), fail_silently=False,
416
				 html_message=html_message)
417 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
418
419
def send_email_changed(request, old_email, new_email):
420
	message = "Dmail been changed for user {}".format(request.user.username)
421
	ip_info = get_or_create_ip(get_client_ip(request), logger)
422
	context = {
423
		'username': request.user.username,
424
		'ipInfo': ip_info.info,
425
		'ip': ip_info.ip,
426
		'timeCreated': datetime.datetime.now(),
427
		'email': new_email,
428
	}
429
	html_message = render_to_string('change_email.html', context, context_instance=RequestContext(request))
430
	send_mail("Pychat: email change", message, request.get_host(), (old_email,), fail_silently=False,
431
				 html_message=html_message)
432
433
434
def extract_photo(image_base64, filename=None):
435
	base64_type_data = re.search(r'data:(\w+/(\w+));base64,(.*)$', image_base64)
436
	logger.debug('Parsing base64 image')
437
	image_data = base64_type_data.group(3)
438
	f = BytesIO(base64.b64decode(image_data))
439
	content_type = base64_type_data.group(1)
440
	name = filename or ".{}".format(base64_type_data.group(2))
441
	logger.debug('Base64 filename extension %s, content_type %s', name, content_type)
442
	image = InMemoryUploadedFile(
443
		f,
444
		field_name='photo',
445
		name=name,
446
		content_type=content_type,
447
		size=sys.getsizeof(f),
448
		charset=None)
449
	return image
450
451
452
def create_user_model(user):
453
	user.save()
454
	RoomUsers(user_id=user.id, room_id=settings.ALL_ROOM_ID, notifications=False).save()
455
	logger.info('Signed up new user %s, subscribed for channels with id %d', user, settings.ALL_ROOM_ID)
456
	return user
457
458
459
def create_ip_structure(ip, raw_response):
460
	response = json.loads(raw_response)
461
	if response['status'] != "success":
462
		raise Exception("Creating iprecord failed, server responded: %s" % raw_response)
463
	return IpAddress.objects.create(
464
		ip=ip,
465
		isp=response['isp'],
466
		country=response['country'],
467
		region=response['regionName'],
468
		city=response['city'],
469
		country_code=response['countryCode']
470
	)
471
472
473
def get_or_create_ip(ip, logger):
474
	def create_ip():
475
		f = urlopen(settings.IP_API_URL % ip)
476
		decode = f.read().decode("utf-8")
477
		return create_ip_structure(ip, decode)
478
	return get_or_create_ip_wrapper(ip, logger, create_ip)
479
480
481
def get_or_create_ip_wrapper(ip, logger, fetcher_ip_function):
482
	"""
483
	@param ip: ip to fetch info from
484
	@param logger initialized logger:
485
	@type IpAddress
486
	"""
487
	try:
488
		return IpAddress.objects.get(ip=ip)
489
	except IpAddress.DoesNotExist:
490
		try:
491
			if not hasattr(settings, 'IP_API_URL'):
492
				raise Exception("IP_API_URL aint set")
493
			return fetcher_ip_function()
494
		except Exception as e:
495
			logger.error("Error while creating ip with country info, because %s", e)
496
			return IpAddress.objects.create(ip=ip)
497
498
499
class EmailOrUsernameModelBackend(object):
500
	"""
501
	This is a ModelBacked that allows authentication with either a username or an email address.
502
	"""
503
504
	def authenticate(self, username=None, password=None):
505
		try:
506
			if '@' in username:
507
				user = UserProfile.objects.get(email=username)
508
			else:
509
				user = UserProfile.objects.get(username=username)
510
			if user.check_password(password):
511
				return user
512
		except User.DoesNotExist:
513
			return None
514
515
	def get_user(self, username):
516
		try:
517
			return get_user_model().objects.get(pk=username)
518
		except get_user_model().DoesNotExist:
519
			return None
520
521
522
def get_max_key(files):
523
	max = None
524
	if files:
525
		for f in files:
526
			if max is None or f.symbol > max:
527
				max = f.symbol
528
	return max
529
530
531
def update_symbols(files, message):
532
	if message.symbol:
533
		order = ord(message.symbol)
534
		for up in files:
535
			if ord(up.symbol) <= order:
536
				order += 1
537
				new_symb = chr(order)
538
				message.content = message.content.replace(up.symbol, new_symb)
539
				up.symbol = new_symb
540
	new_symbol = get_max_key(files)
541
	if message.symbol is None or new_symbol > message.symbol:
542
		message.symbol = new_symbol
543
544
545
def get_message_images_videos(messages):
546
	ids = [message.id for message in messages if message.symbol]
547
	if ids:
548
		images = Image.objects.filter(message_id__in=ids)
549
	else:
550
		images = []
551
	return images
552
553
554
def up_files_to_img(files, message_id):
555
	blk_video = {}
556
	for f in files:
557
		stored_file = blk_video.setdefault(f.symbol, Image(symbol=f.symbol))
558
		if f.type_enum == UploadedFile.UploadedFileChoices.preview:
559
			stored_file.preview = f.file
560
		else:
561
			stored_file.message_id = message_id
562
			stored_file.img = f.file
563
			stored_file.type = f.type
564
	images = Image.objects.bulk_create(dict_values_to_list(blk_video))
565
	files.delete()
566
	return images