Issues (2)

chat/utils.py (2 issues)

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