Completed
Push — master ( 27f06b...c4efa3 )
by Andrew
26s
created

save_ip()   A

Complexity

Conditions 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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