Completed
Push — master ( ec5943...6b5d6f )
by Andrew
30s
created

get_users_in_current_user_rooms()   A

Complexity

Conditions 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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