from django.conf import settings from django.contrib.auth.models import User from zephyr.lib.context_managers import lockfile from zephyr.models import Realm, Stream, UserProfile, \ Subscription, Recipient, Message, UserMessage, \ DefaultStream, \ MAX_MESSAGE_LENGTH, get_client from django.db import transaction, IntegrityError from zephyr.lib.initial_password import initial_password from zephyr.lib.cache import cache_with_key from zephyr.lib.message_cache import cache_save_message from django.utils import timezone from django.contrib.auth.models import UserManager import subprocess import simplejson import time import traceback import re import requests import hashlib import base64 # Store an event in the log for re-importing messages def log_event(event): if "timestamp" not in event: event["timestamp"] = time.time() with lockfile(settings.MESSAGE_LOG + '.lock'): with open(settings.MESSAGE_LOG, 'a') as log: log.write(simplejson.dumps(event) + '\n') # create_user_hack is the same as Django's User.objects.create_user, # except that we don't save to the database so it can used in # bulk_creates def create_user_hack(username, password, email, active): now = timezone.now() email = UserManager.normalize_email(email) user = User(username=username, email=email, is_staff=False, is_active=active, is_superuser=False, last_login=now, date_joined=now) if active: user.set_password(password) else: user.set_unusable_password() return user def create_user_base(email, password, active=True): # NB: the result of Base32 + truncation is not a valid Base32 encoding. # It's just a unique alphanumeric string. # Use base32 instead of base64 so we don't have to worry about mixed case. # Django imposes a limit of 30 characters on usernames. email_hash = hashlib.sha256(settings.HASH_SALT + email).digest() username = base64.b32encode(email_hash)[:30] return create_user_hack(username, password, email, active) def create_user(email, password, realm, full_name, short_name, active=True): user = create_user_base(email=email, password=password, active=active) user.save() return UserProfile.create(user, realm, full_name, short_name) def do_create_user(email, password, realm, full_name, short_name, active=True): log_event({'type': 'user_created', 'timestamp': time.time(), 'full_name': full_name, 'short_name': short_name, 'user': email}) return create_user(email, password, realm, full_name, short_name, active) def compute_mit_user_fullname(email): try: # Input is either e.g. starnine@mit.edu or user|CROSSREALM.INVALID@mit.edu match_user = re.match(r'^([a-zA-Z0-9_.-]+)(\|.+)?@mit\.edu$', email.lower()) if match_user and match_user.group(2) is None: dns_query = "%s.passwd.ns.athena.mit.edu" % (match_user.group(1),) proc = subprocess.Popen(['host', '-t', 'TXT', dns_query], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, _err_unused = proc.communicate() if proc.returncode == 0: # Parse e.g. 'starnine:*:84233:101:Athena Consulting Exchange User,,,:/mit/starnine:/bin/bash' # for the 4th passwd entry field, aka the person's name. hesiod_name = out.split(':')[4].split(',')[0].strip() if hesiod_name == "": return email return hesiod_name elif match_user: return match_user.group(1).lower() + "@" + match_user.group(2).upper()[1:] except: print ("Error getting fullname for %s:" % (email,)) traceback.print_exc() return email.lower() def create_mit_user_if_needed(realm, email): try: return UserProfile.objects.get(user__email=email) except UserProfile.DoesNotExist: try: # Forge a user for this person return create_user(email, initial_password(email), realm, compute_mit_user_fullname(email), email.split("@")[0], active=False) except IntegrityError: # Unless we raced with another thread doing the same # thing, in which case we should get the user they made transaction.commit() return UserProfile.objects.get(user__email=email) def log_message(message): if not message.sending_client.name.startswith("test:"): log_event(message.to_log_dict()) user_hash = {} def get_user_profile_by_id(uid): if uid in user_hash: return user_hash[uid] return UserProfile.objects.select_related().get(id=uid) def do_send_message(message, no_log=False): # Log the message to our message log for populate_db to refill if not no_log: log_message(message) if message.recipient.type == Recipient.PERSONAL: recipients = list(set([get_user_profile_by_id(message.recipient.type_id), get_user_profile_by_id(message.sender_id)])) # For personals, you send out either 1 or 2 copies of the message, for # personals to yourself or to someone else, respectively. assert((len(recipients) == 1) or (len(recipients) == 2)) elif (message.recipient.type == Recipient.STREAM or message.recipient.type == Recipient.HUDDLE): recipients = [s.user_profile for s in Subscription.objects.select_related().filter(recipient=message.recipient, active=True)] else: raise ValueError('Bad recipient type') # Save the message receipts in the database # TODO: Use bulk_create here with transaction.commit_on_success(): message.save() for user_profile in recipients: # Only deliver messages to "active" user accounts if user_profile.user.is_active: UserMessage(user_profile=user_profile, message=message).save() cache_save_message(message) # We can only publish messages to longpolling clients if the Tornado server is running. if settings.TORNADO_SERVER: # Render Markdown etc. here, so that the single-threaded Tornado server doesn't have to. # TODO: Reduce duplication in what we send. rendered = { 'text/html': message.to_dict(apply_markdown=True), 'text/x-markdown': message.to_dict(apply_markdown=False) } requests.post(settings.TORNADO_SERVER + '/notify_new_message', data=dict( secret = settings.SHARED_SECRET, message = message.id, rendered = simplejson.dumps(rendered), users = simplejson.dumps([str(user.id) for user in recipients]))) def create_stream_if_needed(realm, stream_name): (stream, created) = Stream.objects.get_or_create( realm=realm, name__iexact=stream_name, defaults={'name': stream_name}) if created: Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM) return stream def internal_send_message(sender_email, recipient_type, recipient_name, subject, content): if len(content) > MAX_MESSAGE_LENGTH: content = content[0:3900] + "\n\n[message was too long and has been truncated]" message = Message() message.sender = UserProfile.objects.get(user__email=sender_email) message.recipient = Recipient.objects.get(type_id=create_stream_if_needed( message.sender.realm, recipient_name).id, type=recipient_type) message.subject = subject message.content = content message.pub_date = timezone.now() message.sending_client = get_client("Internal") do_send_message(message) def do_add_subscription(user_profile, stream, no_log=False): recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM) (subscription, created) = Subscription.objects.get_or_create( user_profile=user_profile, recipient=recipient, defaults={'active': True}) did_subscribe = created if not subscription.active: did_subscribe = True subscription.active = True subscription.save() if did_subscribe and not no_log: log_event({'type': 'subscription_added', 'user': user_profile.user.email, 'name': stream.name, 'domain': stream.realm.domain}) return did_subscribe def do_remove_subscription(user_profile, stream, no_log=False): recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM) maybe_sub = Subscription.objects.filter(user_profile=user_profile, recipient=recipient) if len(maybe_sub) == 0: return False subscription = maybe_sub[0] did_remove = subscription.active subscription.active = False subscription.save() if did_remove and not no_log: log_event({'type': 'subscription_removed', 'user': user_profile.user.email, 'name': stream.name, 'domain': stream.realm.domain}) return did_remove def log_subscription_property_change(user_email, property, property_dict): event = {'type': 'subscription_property', 'property': property, 'user': user_email} event.update(property_dict) log_event(event) def do_activate_user(user, log=True, join_date=timezone.now()): user.is_active = True user.set_password(initial_password(user.email)) user.date_joined = join_date user.save() if log: log_event({'type': 'user_activated', 'user': user.email}) def do_change_password(user, password, log=True, commit=True): user.set_password(password) if commit: user.save() if log: log_event({'type': 'user_change_password', 'user': user.email, 'pwhash': user.password}) def do_change_full_name(user_profile, full_name, log=True): user_profile.full_name = full_name user_profile.save() if log: log_event({'type': 'user_change_full_name', 'user': user_profile.user.email, 'full_name': full_name}) def do_create_realm(domain, replay=False): realm, created = Realm.objects.get_or_create(domain=domain) if created and not replay: # Log the event log_event({"type": "realm_created", "domain": domain}) # Sent a notification message message = Message() message.sender = UserProfile.objects.get(user__email="humbug+signups@humbughq.com") message.recipient = Recipient.objects.get(type_id=create_stream_if_needed( message.sender.realm, "signups").id, type=Recipient.STREAM) message.subject = domain message.content = "Signups enabled." message.pub_date = timezone.now() message.sending_client = get_client("Internal") do_send_message(message) return (realm, created) def do_change_enable_desktop_notifications(user_profile, enable_desktop_notifications, log=True): user_profile.enable_desktop_notifications = enable_desktop_notifications user_profile.save() if log: log_event({'type': 'enable_desktop_notifications_changed', 'user': user_profile.user.email, 'enable_desktop_notifications': enable_desktop_notifications}) def set_default_streams(realm, stream_names): DefaultStream.objects.filter(realm=realm).delete() for stream_name in stream_names: stream = create_stream_if_needed(realm, stream_name) DefaultStream.objects.create(stream=stream, realm=realm) def add_default_subs(user_profile): for default in DefaultStream.objects.filter(realm=user_profile.realm): do_add_subscription(user_profile, default.stream)