Completed
Push — master ( b307ec...4c8bc3 )
by Andrew
32s
created

get_user_by_code()   B

Complexity

Conditions 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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