def ResetPassword(self, request, context): """ If the user does not exist, do nothing. If the user exists, we send them an email. If they have a password, clicking that email will remove the password. If they don't have a password, it sends them an email saying someone tried to reset the password but there was none. Note that as long as emails are send synchronously, this is far from constant time regardless of output. """ with session_scope() as session: user = session.execute( select(User).where_username_or_email(request.user).where( ~User.is_deleted)).scalar_one_or_none() if user: send_password_reset_email(session, user) notify( user_id=user.id, topic="account_recovery", key="", action="start", icon="wrench", title=f"Password reset initiated", link=urls.account_settings_link(), ) else: # user not found logger.debug(f"Didn't find user") return empty_pb2.Empty()
def CompletePasswordReset(self, request, context): """ Completes the password reset: just clears the user's password """ with session_scope() as session: res = session.execute( select(PasswordResetToken, User).join( User, User.id == PasswordResetToken.user_id).where( PasswordResetToken.token == request.password_reset_token).where( PasswordResetToken.is_valid)).one_or_none() if res: password_reset_token, user = res session.delete(password_reset_token) user.hashed_password = None session.commit() notify( user_id=user.id, topic="account_recovery", key="", action="complete", icon="wrench", title=f"Password reset completed", link=urls.account_settings_link(), ) return empty_pb2.Empty() else: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN)
def CreateApiKey(self, request, context): with session_scope() as session: user = session.execute( select(User).where_username_or_email_or_id( request.user)).scalar_one_or_none() if not user: context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) token, expiry = create_session(context, session, user, long_lived=True, is_api_key=True, duration=timedelta(days=365)) send_api_key_email(session, user, token, expiry) notify( user_id=user.id, topic="api_key", key="", action="create", icon="wrench", title= f"An admin created an API key for you, please check your email", link=urls.account_settings_link(), ) return _user_to_details(user)
def SendFriendRequest(self, request, context): if context.user_id == request.user_id: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_FRIEND_SELF) with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() to_user = session.execute( select(User).where_users_visible(context).where( User.id == request.user_id)).scalar_one_or_none() if not to_user: context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) if (session.execute( select(FriendRelationship).where( or_( and_( FriendRelationship.from_user_id == context.user_id, FriendRelationship.to_user_id == request.user_id, ), and_( FriendRelationship.from_user_id == request.user_id, FriendRelationship.to_user_id == context.user_id, ), )). where( or_( FriendRelationship.status == FriendStatus.accepted, FriendRelationship.status == FriendStatus.pending, ))).scalar_one_or_none() is not None): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.FRIENDS_ALREADY_OR_PENDING) # TODO: Race condition where we can create two friend reqs, needs db constraint! See comment in table friend_relationship = FriendRelationship( from_user=user, to_user=to_user, status=FriendStatus.pending) session.add(friend_relationship) session.commit() send_friend_request_email(friend_relationship) notify( user_id=friend_relationship.to_user_id, topic="friend_request", key=str(friend_relationship.from_user_id), action="send", avatar_key=user.avatar.thumbnail_url if user.avatar else None, icon="person", title=f"**{user.name}** sent you a friend request", link=urls.friend_requests_link(), ) return empty_pb2.Empty()
def ConfirmChangeEmail(self, request, context): with session_scope() as session: user_with_valid_token_from_old_email = session.execute( select(User).where( User.old_email_token == request.change_email_token).where( User.old_email_token_created <= now()).where( User.old_email_token_expiry >= now()) ).scalar_one_or_none() user_with_valid_token_from_new_email = session.execute( select(User).where( User.new_email_token == request.change_email_token).where( User.new_email_token_created <= now()).where( User.new_email_token_expiry >= now()) ).scalar_one_or_none() if user_with_valid_token_from_old_email: user = user_with_valid_token_from_old_email user.old_email_token = None user.old_email_token_created = None user.old_email_token_expiry = None user.need_to_confirm_via_old_email = False elif user_with_valid_token_from_new_email: user = user_with_valid_token_from_new_email user.new_email_token = None user.new_email_token_created = None user.new_email_token_expiry = None user.need_to_confirm_via_new_email = False else: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) # Using "___ is False" instead of "not ___" so that "None" doesn't pass if user.need_to_confirm_via_old_email is False and user.need_to_confirm_via_new_email is False: user.email = user.new_email user.new_email = None user.need_to_confirm_via_old_email = None user.need_to_confirm_via_new_email = None notify( user_id=user.id, topic="email_address", key="", action="change", icon="wrench", title=f"Your email was changed", link=urls.account_settings_link(), ) return auth_pb2.ConfirmChangeEmailRes( state=auth_pb2.EMAIL_CONFIRMATION_STATE_SUCCESS) elif user.need_to_confirm_via_old_email: return auth_pb2.ConfirmChangeEmailRes( state=auth_pb2. EMAIL_CONFIRMATION_STATE_REQUIRES_CONFIRMATION_FROM_OLD_EMAIL ) else: return auth_pb2.ConfirmChangeEmailRes( state=auth_pb2. EMAIL_CONFIRMATION_STATE_REQUIRES_CONFIRMATION_FROM_NEW_EMAIL )
def VerifyPhone(self, request, context): if not sms.looks_like_a_code(request.token): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.WRONG_SMS_CODE) with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() if user.phone_verification_token is None: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NO_PENDING_VERIFICATION) if now() - user.phone_verification_sent > SMS_CODE_LIFETIME: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NO_PENDING_VERIFICATION) if user.phone_verification_attempts > SMS_CODE_ATTEMPTS: context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, errors.TOO_MANY_SMS_CODE_ATTEMPTS) if not verify_token(request.token, user.phone_verification_token): user.phone_verification_attempts += 1 session.commit() context.abort(grpc.StatusCode.NOT_FOUND, errors.WRONG_SMS_CODE) # Delete verifications from everyone else that has this number session.execute( update(User).where(User.phone == user.phone).where( User.id != context.user_id).values({ "phone_verification_verified": None, "phone_verification_attempts": 0, "phone_verification_token": None, "phone": None, }).execution_options(synchronize_session=False)) user.phone_verification_token = None user.phone_verification_verified = now() user.phone_verification_attempts = 0 notify( user_id=user.id, topic="phone_number", key="", action="verify", icon="wrench", title=f"Your phone number was verified", link=urls.account_settings_link(), ) return empty_pb2.Empty()
def ChangePhone(self, request, context): phone = request.phone # early quick validation if phone and not is_e164_format(phone): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_PHONE) with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() if not phone: user.phone = None user.phone_verification_verified = None user.phone_verification_token = None user.phone_verification_attempts = 0 return empty_pb2.Empty() if not is_known_operator(phone): context.abort(grpc.StatusCode.UNIMPLEMENTED, errors.UNRECOGNIZED_PHONE_NUMBER) if now( ) - user.phone_verification_sent < PHONE_REVERIFICATION_INTERVAL: context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, errors.REVERIFICATION_TOO_EARLY) token = sms.generate_random_code() result = sms.send_sms(phone, sms.format_message(token)) if result == "success": user.phone = phone user.phone_verification_verified = None user.phone_verification_token = token user.phone_verification_sent = now() user.phone_verification_attempts = 0 notify( user_id=user.id, topic="phone_number", key="", action="change", icon="wrench", title=f"Your phone number was changed", link=urls.account_settings_link(), ) return empty_pb2.Empty() context.abort(grpc.StatusCode.UNIMPLEMENTED, result)
def process_generate_message_notifications(payload): """ Generates notifications for a message sent to a group chat """ logger.info(f"Sending out notifications for message_id = {payload.message_id}") with session_scope() as session: message, group_chat = session.execute( select(Message, GroupChat) .join(GroupChat, GroupChat.conversation_id == Message.conversation_id) .where(Message.id == payload.message_id) ).one() if message.message_type != MessageType.text: logger.info(f"Not a text message, not notifying. message_id = {payload.message_id}") return subscriptions = ( session.execute( select(GroupChatSubscription) .join(User, User.id == GroupChatSubscription.user_id) .where(GroupChatSubscription.group_chat_id == message.conversation_id) .where(User.is_visible) .where(User.id != message.author_id) .where(GroupChatSubscription.left == None) .where(not_(GroupChatSubscription.is_muted)) ) .scalars() .all() ) for subscription in subscriptions: logger.info(f"Notifying user_id = {subscription.user_id}") notify( user_id=subscription.user_id, topic="chat", key=str(message.conversation_id), action="message", icon="message", title=f"{message.author.name} sent a message in {group_chat.title}", content=message.text, link=urls.chat_link(chat_id=message.conversation_id), )
def ChangePassword(self, request, context): """ Changes the user's password. They have to confirm their old password just in case. If they didn't have an old password previously, then we don't check that. """ with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() if not request.HasField("old_password") and not request.HasField( "new_password"): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_BOTH_PASSWORDS) _check_password(user, "old_password", request, context) # password correct or no password if not request.HasField("new_password"): # the user wants to unset their password user.hashed_password = None else: abort_on_invalid_password(request.new_password.value, context) user.hashed_password = hash_password( request.new_password.value) session.commit() send_password_changed_email(user) notify( user_id=user.id, topic="password", key="", action="change", icon="wrench", title=f"Your password was changed", link=urls.account_settings_link(), ) return empty_pb2.Empty()
def ChangeUserBirthdate(self, request, context): with session_scope() as session: user = session.execute( select(User).where_username_or_email_or_id( request.user)).scalar_one_or_none() if not user: context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) user.birthdate = parse_date(request.birthdate) notify( user_id=user.id, topic="birthdate", key="", action="change", icon="wrench", title=f"An admin changed your birth date", link=urls.account_settings_link(), ) return _user_to_details(user)
def RespondFriendRequest(self, request, context): with session_scope() as session: friend_request = session.execute( select(FriendRelationship).where_users_column_visible( context, FriendRelationship.from_user_id). where(FriendRelationship.to_user_id == context.user_id).where( FriendRelationship.status == FriendStatus.pending).where( FriendRelationship.id == request.friend_request_id)).scalar_one_or_none() if not friend_request: context.abort(grpc.StatusCode.NOT_FOUND, errors.FRIEND_REQUEST_NOT_FOUND) friend_request.status = FriendStatus.accepted if request.accept else FriendStatus.rejected friend_request.time_responded = func.now() if friend_request.status == FriendStatus.accepted: send_friend_request_accepted_email(friend_request) session.commit() if friend_request.status == FriendStatus.accepted: notify( user_id=friend_request.from_user_id, topic="friend_request", key=str(friend_request.to_user_id), action="accept", avatar_key=friend_request.to_user.avatar.thumbnail_url if friend_request.to_user.avatar else None, icon="person", title= f"**{friend_request.from_user.name}** accepted your friend request", link=urls.user_link( username=friend_request.to_user.username), ) return empty_pb2.Empty()
def ChangeEmail(self, request, context): """ Change the user's email address. If the user has a password, a notification is sent to the old email, and a confirmation is sent to the new one. Otherwise they need to confirm twice, via an email sent to each of their old and new emails. In all confirmation emails, the user must click on the confirmation link. """ with session_scope() as session: user = session.execute( select(User).where(User.id == context.user_id)).scalar_one() # check password first _check_password(user, "password", request, context) # not a valid email if not is_valid_email(request.new_email): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) # email already in use (possibly by this user) if session.execute( select(User).where( User.email == request.new_email)).scalar_one_or_none(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) user.new_email = request.new_email user.new_email_token = urlsafe_secure_token() user.new_email_token_created = now() user.new_email_token_expiry = now() + timedelta(hours=2) user.need_to_confirm_via_new_email = True if user.has_password: user.old_email_token = None user.old_email_token_created = None user.old_email_token_expiry = None user.need_to_confirm_via_old_email = False send_email_changed_notification_email(user) send_email_changed_confirmation_to_new_email(user) notify( user_id=user.id, topic="email_address", key="", action="change", icon="wrench", title=f"Your email was changed", link=urls.account_settings_link(), ) else: user.old_email_token = urlsafe_secure_token() user.old_email_token_created = now() user.old_email_token_expiry = now() + timedelta(hours=2) user.need_to_confirm_via_old_email = True send_email_changed_confirmation_to_old_email(user) send_email_changed_confirmation_to_new_email(user) # session autocommit return empty_pb2.Empty()
def CreateHostRequest(self, request, context): with session_scope() as session: if request.host_user_id == context.user_id: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.CANT_REQUEST_SELF) # just to check host exists and is visible host = session.execute( select(User).where_users_visible(context).where( User.id == request.host_user_id)).scalar_one_or_none() if not host: context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) from_date = parse_date(request.from_date) to_date = parse_date(request.to_date) if not from_date or not to_date: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_DATE) today = today_in_timezone(host.timezone) # request starts from the past if from_date < today: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_BEFORE_TODAY) # from_date is not >= to_date if from_date >= to_date: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_AFTER_TO) # No need to check today > to_date if from_date - today > timedelta(days=365): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_FROM_AFTER_ONE_YEAR) if to_date - from_date > timedelta(days=365): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.DATE_TO_AFTER_ONE_YEAR) conversation = Conversation() session.add(conversation) session.flush() session.add( Message( conversation_id=conversation.id, author_id=context.user_id, message_type=MessageType.chat_created, )) message = Message( conversation_id=conversation.id, author_id=context.user_id, text=request.text, message_type=MessageType.text, ) session.add(message) session.flush() host_request = HostRequest( conversation_id=conversation.id, surfer_user_id=context.user_id, host_user_id=host.id, from_date=from_date, to_date=to_date, status=HostRequestStatus.pending, surfer_last_seen_message_id=message.id, # TODO: tz # timezone=host.timezone, ) session.add(host_request) session.commit() send_new_host_request_email(host_request) notify( user_id=host_request.host_user_id, topic="host_request", action="create", key=str(host_request.surfer_user_id), avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, title= f"**{host_request.surfer.name}** sent you a hosting request", content=request.text, link=urls.host_request_link_host(), ) return requests_pb2.CreateHostRequestRes( host_request_id=host_request.conversation_id)
def SendHostRequestMessage(self, request, context): if request.text == "": context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE) with session_scope() as session: host_request = session.execute( select(HostRequest).where( HostRequest.conversation_id == request.host_request_id)).scalar_one_or_none() if not host_request: context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) if host_request.status == HostRequestStatus.rejected or host_request.status == HostRequestStatus.cancelled: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.HOST_REQUEST_CLOSED) if host_request.end_time < now(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST) message = Message() message.conversation_id = host_request.conversation_id message.author_id = context.user_id message.message_type = MessageType.text message.text = request.text session.add(message) session.flush() if host_request.surfer_user_id == context.user_id: host_request.surfer_last_seen_message_id = message.id notify( user_id=host_request.host_user_id, topic="host_request", action="message", key=str(host_request.surfer_user_id), avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, title= f"**{host_request.surfer.name}** sent a message in their host request", link=urls.host_request_link_host(), ) else: host_request.host_last_seen_message_id = message.id notify( user_id=host_request.surfer_user_id, topic="host_request", action="message", key=str(host_request.host_user_id), avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, title= f"**{host_request.host.name}** sent a message in your host request", link=urls.host_request_link_guest(), ) session.commit() return empty_pb2.Empty()
def RespondHostRequest(self, request, context): with session_scope() as session: host_request = session.execute( select(HostRequest).where_users_column_visible( context, HostRequest.surfer_user_id).where_users_column_visible( context, HostRequest.host_user_id).where( HostRequest.conversation_id == request.host_request_id)).scalar_one_or_none() if not host_request: context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) if host_request.surfer_user_id != context.user_id and host_request.host_user_id != context.user_id: context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) if request.status == conversations_pb2.HOST_REQUEST_STATUS_PENDING: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) if host_request.end_time < now(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOST_REQUEST_IN_PAST) control_message = Message() if request.status == conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED: # only host can accept if context.user_id != host_request.host_user_id: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.NOT_THE_HOST) # can't accept a cancelled or confirmed request (only reject), or already accepted if (host_request.status == HostRequestStatus.cancelled or host_request.status == HostRequestStatus.confirmed or host_request.status == HostRequestStatus.accepted): context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) control_message.host_request_status_target = HostRequestStatus.accepted host_request.status = HostRequestStatus.accepted send_host_request_accepted_email_to_guest(host_request) notify( user_id=host_request.surfer_user_id, topic="host_request", action="accept", key=str(host_request.host_user_id), avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, title= f"**{host_request.host.name}** accepted your host request", link=urls.host_request_link_guest(), ) if request.status == conversations_pb2.HOST_REQUEST_STATUS_REJECTED: # only host can reject if context.user_id != host_request.host_user_id: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) # can't reject a cancelled or already rejected request if (host_request.status == HostRequestStatus.cancelled or host_request.status == HostRequestStatus.rejected): context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) control_message.host_request_status_target = HostRequestStatus.rejected host_request.status = HostRequestStatus.rejected send_host_request_rejected_email_to_guest(host_request) notify( user_id=host_request.surfer_user_id, topic="host_request", action="reject", key=str(host_request.host_user_id), avatar_key=host_request.host.avatar.thumbnail_url if host_request.host.avatar else None, title= f"**{host_request.host.name}** rejected your host request", link=urls.host_request_link_guest(), ) if request.status == conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED: # only hostee can confirm if context.user_id != host_request.surfer_user_id: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) # can only confirm an accepted request if host_request.status != HostRequestStatus.accepted: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) control_message.host_request_status_target = HostRequestStatus.confirmed host_request.status = HostRequestStatus.confirmed send_host_request_confirmed_email_to_host(host_request) notify( user_id=host_request.host_user_id, topic="host_request", action="confirm", key=str(host_request.surfer_user_id), avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, title= f"**{host_request.surfer.name}** confirmed their host request", link=urls.host_request_link_host(), ) if request.status == conversations_pb2.HOST_REQUEST_STATUS_CANCELLED: # only hostee can cancel if context.user_id != host_request.surfer_user_id: context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) # can't' cancel an already cancelled or rejected request if (host_request.status == HostRequestStatus.rejected or host_request.status == HostRequestStatus.cancelled): context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVALID_HOST_REQUEST_STATUS) control_message.host_request_status_target = HostRequestStatus.cancelled host_request.status = HostRequestStatus.cancelled send_host_request_cancelled_email_to_host(host_request) notify( user_id=host_request.host_user_id, topic="host_request", action="cancel", key=str(host_request.surfer_user_id), avatar_key=host_request.surfer.avatar.thumbnail_url if host_request.surfer.avatar else None, title= f"**{host_request.surfer.name}** cancelled their host request", link=urls.host_request_link_host(), ) control_message.message_type = MessageType.host_request_status_changed control_message.conversation_id = host_request.conversation_id control_message.author_id = context.user_id session.add(control_message) if request.text: latest_message = Message() latest_message.conversation_id = host_request.conversation_id latest_message.text = request.text latest_message.author_id = context.user_id latest_message.message_type = MessageType.text session.add(latest_message) else: latest_message = control_message session.flush() if host_request.surfer_user_id == context.user_id: host_request.surfer_last_seen_message_id = latest_message.id else: host_request.host_last_seen_message_id = latest_message.id session.commit() return empty_pb2.Empty()