Completed
Push — master ( 833c24...71b3aa )
by Andrew
24s
created

with_history_q()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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