Completed
Push — master ( ec7895...07bc46 )
by Andrew
28s
created

create_self_room()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
dl 0
loc 12
rs 9.4285
1
import base64
2
import json
3
import logging
4
import re
5
import sys
6
from io import BytesIO
7
8
import requests
9
from django.contrib.auth import get_user_model
10
from django.core.exceptions import ValidationError
11
from django.core.files.uploadedfile import InMemoryUploadedFile
12
from django.core.mail import send_mail
13
from django.core.validators import validate_email
14
from django.db import IntegrityError
15
from django.db import connection, OperationalError, InterfaceError
16
from django.template import RequestContext
17
from django.template.loader import render_to_string
18
from django.utils.safestring import mark_safe
19
20
from chat import local
21
from chat import settings
22
from chat.models import Room, get_milliseconds
23
from chat.models import User
24
from chat.models import UserProfile, Verification, RoomUsers, IpAddress
25
from chat.py2_3 import urlopen
26
from chat.settings import ISSUES_REPORT_LINK, SITE_PROTOCOL, ALL_ROOM_ID, SELECT_SELF_ROOM
27
from chat.tornado.constants import RedisPrefix
28
from chat.tornado.constants import VarNames
29
30
USERNAME_REGEX = str(settings.MAX_USERNAME_LENGTH).join(['^[a-zA-Z-_0-9]{1,', '}$'])
31
API_URL = getattr(settings, "IP_API_URL", None)
32
RECAPTCHA_SECRET_KEY = getattr(settings, "RECAPTCHA_SECRET_KEY", None)
33
GOOGLE_OAUTH_2_CLIENT_ID = getattr(settings, "GOOGLE_OAUTH_2_CLIENT_ID", None)
34
GOOGLE_OAUTH_2_HOST = getattr(settings, "GOOGLE_OAUTH_2_HOST", None)
35
FACEBOOK_ACCESS_TOKEN = getattr(settings, "FACEBOOK_ACCESS_TOKEN", None)
36
37
logger = logging.getLogger(__name__)
38
39
40
def is_blank(check_str):
41
	if check_str and check_str.strip():
42
		return False
43
	else:
44
		return True
45
46
47
def get_users_in_current_user_rooms(user_id):
48
	user_rooms = Room.objects.filter(users__id=user_id, disabled=False).values('id', 'name')
49
	res = {room['id']: {
50
			VarNames.ROOM_NAME: room['name'],
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 = 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(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).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.time + 600000 < get_milliseconds():
148
		raise ValidationError("You can only edit messages that were send not more than 10 min ago")
149
	if message.deleted:
150
		raise ValidationError("Already deleted")
151
152
153
def do_db(callback, *args, **kwargs):
154
	try:
155
		return callback(*args, **kwargs)
156
	except (OperationalError, InterfaceError) as e:
157
		if 'MySQL server has gone away' in str(e):
158
			logger.warning('%s, reconnecting' % e)
159
			connection.close()
160
			return callback(*args, **kwargs)
161
		else:
162
			raise e
163
164
165
def execute_query(query, *args, **kwargs):
166
	cursor = connection.cursor()
167
	cursor.execute(query, *args, **kwargs)
168
	desc = cursor.description
169
	return [
170
		dict(zip([col[0] for col in desc], row))
171
		for row in cursor.fetchall()
172
	]
173
174
175
def hide_fields(post, fields, huge=False, fill_with='****'):
176
	"""
177
	:param post: Object that will be copied
178
	:type post: QueryDict
179
	:param fields: fields that will be removed
180
	:param fill_with: replace characters for hidden fields
181
	:param huge: if true object will be cloned and then fields will be removed
182
	:return: a shallow copy of dictionary without specified fields
183
	"""
184
	if not huge:
185
		# hide *fields in shallow copy
186
		res = post.copy()
187
		for field in fields:
188
			if field in post:  # check if field was absent
189
				res[field] = fill_with
190
	else:
191
		# copy everything but *fields
192
		res = {}
193
		for field in post:
194
			# _______________________if this is field to remove
195
			res[field] = post[field] if field not in fields else fill_with
196
	return res
197
198
199
def check_password(password):
200
	"""
201
	Checks if password is secure
202
	:raises ValidationError exception if password is not valid
203
	"""
204
	if is_blank(password):
205
		raise ValidationError("password can't be empty")
206
	if not re.match(u'^\S.+\S$', password):
207
		raise ValidationError("password should be at least 3 symbols")
208
209
210
def check_email(email):
211
	"""
212
	:raises ValidationError if specified email is registered or not valid
213
	"""
214
	if not email:
215
		return
216
	try:
217
		validate_email(email)
218
		# theoretically can throw returning 'more than 1' error
219
		UserProfile.objects.get(email=email)
220
		raise ValidationError('Email {} is already used'.format(email))
221
	except User.DoesNotExist:
222
		pass
223
224
225
def check_user(username):
226
	"""
227
	Checks if specified username is free to register
228
	:type username str
229
	:raises ValidationError exception if username is not valid
230
	"""
231
	# Skip javascript validation, only summary message
232
	if is_blank(username):
233
		raise ValidationError("Username can't be empty")
234
	if not re.match(USERNAME_REGEX, username):
235
		raise ValidationError("Username {} doesn't match regex {}".format(username, USERNAME_REGEX))
236
	try:
237
		# theoretically can throw returning 'more than 1' error
238
		User.objects.get(username=username)
239
		raise ValidationError("Username {} is already used. Please select another one".format(username))
240
	except User.DoesNotExist:
241
		pass
242
243
244
def get_client_ip(request):
245
	x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
246
	return x_forwarded_for.split(',')[-1].strip() if x_forwarded_for else request.META.get('REMOTE_ADDR')
247
248
249
def check_captcha(request):
250
	"""
251
	:type request: WSGIRequest
252
	:raises ValidationError: if captcha is not valid or not set
253
	If RECAPTCHA_SECRET_KEY is enabled in settings validates request with it
254
	"""
255
	if not RECAPTCHA_SECRET_KEY:
256
		logger.debug('Skipping captcha validation')
257
		return
258
	try:
259
		captcha_rs = request.POST.get('g-recaptcha-response')
260
		url = "https://www.google.com/recaptcha/api/siteverify"
261
		params = {
262
			'secret': RECAPTCHA_SECRET_KEY,
263
			'response': captcha_rs,
264
			'remoteip': local.client_ip
265
		}
266
		raw_response = requests.post(url, params=params, verify=True)
267
		response = raw_response.json()
268
		if not response.get('success', False):
269
			logger.debug('Captcha is NOT valid, response: %s', raw_response)
270
			raise ValidationError(
271
				response['error-codes'] if response.get('error-codes', None) else 'This captcha already used')
272
		logger.debug('Captcha is valid, response: %s', raw_response)
273
	except Exception as e:
274
		raise ValidationError('Unable to check captcha because {}'.format(e))
275
276
277
def send_sign_up_email(user, site_address, request):
278
	if user.email is not None:
279
		verification = Verification(user=user, type_enum=Verification.TypeChoices.register)
280
		verification.save()
281
		user.email_verification = verification
282
		user.save(update_fields=['email_verification'])
283
		link = "{}://{}/confirm_email?token={}".format(SITE_PROTOCOL, site_address, verification.token)
284
		text = ('Hi {}, you have registered pychat'
285
				  '\nTo complete your registration please click on the url bellow: {}'
286
				  '\n\nIf you find any bugs or propositions you can post them {}').format(
287
			user.username, link, ISSUES_REPORT_LINK)
288
		start_message = mark_safe((
289
			"You have registered in <b>Pychat</b>. If you find any bugs or propositions you can post them"
290
			" <a href='{}'>here</a>. To complete your registration please click on the link below.").format(
291
				ISSUES_REPORT_LINK))
292
		context = {
293
			'username': user.username,
294
			'magicLink': link,
295
			'btnText': "CONFIRM SIGN UP",
296
			'greetings': start_message
297
		}
298
		html_message = render_to_string('sign_up_email.html', context, context_instance=RequestContext(request))
299
		logger.info('Sending verification email to userId %s (email %s)', user.id, user.email)
300
		send_mail("Confirm chat registration", text, site_address, [user.email, ], html_message=html_message)
301
		logger.info('Email %s has been sent', user.email)
302
303
304
def send_reset_password_email(request, user_profile, verification):
305
	link = "{}://{}/restore_password?token={}".format(SITE_PROTOCOL, request.get_host(), verification.token)
306
	message = "{},\n" \
307
				 "You requested to change a password on site {}.\n" \
308
				 "To proceed click on the link {}\n" \
309
				 "If you didn't request the password change just ignore this mail" \
310
		.format(user_profile.username, request.get_host(), link)
311
	ip_info = get_or_create_ip(get_client_ip(request), logger)
312
	start_message = mark_safe(
313
		"You have requested to send you a magic link to quickly restore password to <b>Pychat</b>. "
314
		"If it wasn't you, you can safely ignore this email")
315
	context = {
316
		'username': user_profile.username,
317
		'magicLink': link,
318
		'ipInfo': ip_info.info,
319
		'ip': ip_info.ip,
320
		'btnText': "CHANGE PASSWORD",
321
		'timeCreated': verification.time,
322
		'greetings': start_message
323
	}
324
	html_message = render_to_string('reset_pass_email.html', context, context_instance=RequestContext(request))
325
	send_mail("Pychat: restore password", message, request.get_host(), (user_profile.email,), fail_silently=False,
326
				 html_message=html_message)
327
328
329
def extract_photo(image_base64, filename=None):
330
	base64_type_data = re.search(r'data:(\w+/(\w+));base64,(.*)$', image_base64)
331
	logger.debug('Parsing base64 image')
332
	image_data = base64_type_data.group(3)
333
	f = BytesIO(base64.b64decode(image_data))
334
	content_type = base64_type_data.group(1)
335
	name = filename or ".{}".format(base64_type_data.group(2))
336
	logger.debug('Base64 filename extension %s, content_type %s', name, content_type)
337
	image = InMemoryUploadedFile(
338
		f,
339
		field_name='photo',
340
		name=name,
341
		content_type=content_type,
342
		size=sys.getsizeof(f),
343
		charset=None)
344
	return image
345
346
347
def get_max_key(dictionary):
348
	return max(dictionary.keys()) if dictionary else None
349
350
351
def create_user_model(user):
352
	user.save()
353
	RoomUsers(user_id=user.id, room_id=ALL_ROOM_ID).save()
354
	logger.info('Signed up new user %s, subscribed for channels with id %d', user, ALL_ROOM_ID)
355
	return user
356
357
358
def get_or_create_ip(ip, logger):
359
	"""
360
361
	@param ip: ip to fetch info from
362
	@param logger initialized logger:
363
	@type IpAddress
364
	"""
365
	try:
366
		ip_address = IpAddress.objects.get(ip=ip)
367
	except IpAddress.DoesNotExist:
368
		try:
369
			if not API_URL:
370
				raise Exception('api url is absent')
371
			logger.debug("Creating ip record %s", ip)
372
			f = urlopen(API_URL % ip)
373
			raw_response = f.read().decode("utf-8")
374
			response = json.loads(raw_response)
375
			if response['status'] != "success":
376
				raise Exception("Creating iprecord failed, server responded: %s" % raw_response)
377
			ip_address = IpAddress.objects.create(
378
				ip=ip,
379
				isp=response['isp'],
380
				country=response['country'],
381
				region=response['regionName'],
382
				city=response['city'],
383
				country_code=response['countryCode']
384
			)
385
		except Exception as e:
386
			logger.error("Error while creating ip with country info, because %s", e)
387
			ip_address = IpAddress.objects.create(ip=ip)
388
	return ip_address
389
390
391
class EmailOrUsernameModelBackend(object):
392
	"""
393
	This is a ModelBacked that allows authentication with either a username or an email address.
394
	"""
395
396
	def authenticate(self, username=None, password=None):
397
		try:
398
			if '@' in username:
399
				user = UserProfile.objects.get(email=username)
400
			else:
401
				user = UserProfile.objects.get(username=username)
402
			if user.check_password(password):
403
				return user
404
		except User.DoesNotExist:
405
			return None
406
407
	def get_user(self, username):
408
		try:
409
			return get_user_model().objects.get(pk=username)
410
		except get_user_model().DoesNotExist:
411
			return None
412