<commit_before>package im.actor.server.api.rpc.service.contacts

import java.security.MessageDigest

import scala.collection.immutable
import scala.concurrent._
import scala.concurrent.duration._

import akka.actor._
import akka.util.Timeout
import scodec.bits.BitVector
import slick.dbio.DBIO
import slick.driver.PostgresDriver.api._

import im.actor.api.rpc._
import im.actor.api.rpc.contacts._
import im.actor.api.rpc.misc._
import im.actor.api.rpc.users.{ UpdateUserLocalNameChanged, User }
import im.actor.server.api.util
import im.actor.server.api.util.{ ContactsUtils, PhoneNumber, UserUtils }
import im.actor.server.push.{ SeqUpdatesManager, SeqUpdatesManagerRegion }
import im.actor.server.social.{ SocialManager, SocialManagerRegion }
import im.actor.server.{ models, persist }

class ContactsServiceImpl(implicit
  val seqUpdManagerRegion: SeqUpdatesManagerRegion,
                          val socialManagerRegion: SocialManagerRegion,
                          db:                      Database,
                          actorSystem:             ActorSystem)
  extends ContactsService {

  import ContactsUtils._
  import UserUtils._
  import SocialManager._
  import SeqUpdatesManager._

  override implicit val ec: ExecutionContext = actorSystem.dispatcher
  implicit val timeout = Timeout(5.seconds)

  object Errors {
    val CantAddSelf = RpcError(401, "OWN_USER_ID", "User id cannot be equal to self.", false, None)
    val ContactAlreadyExists = RpcError(400, "CONTACT_ALREADY_EXISTS", "Contact already exists.", false, None)
    val ContactNotFound = RpcError(404, "CONTACT_NOT_FOUND", "Contact not found.", false, None)
  }

  private[service] def hashIds(ids: Seq[Int]): String = {
    val md = MessageDigest.getInstance("SHA-256")
    val uids = ids.to[immutable.SortedSet].mkString(",")
    BitVector(md.digest(uids.getBytes)).toHex
  }

  override def jhandleImportContacts(phones: Vector[PhoneToImport], emails: Vector[EmailToImport], clientData: ClientData): Future[HandlerResult[ResponseImportContacts]] = {
    val action = requireAuth(clientData).map { implicit client ⇒
      // TODO: flatten

      persist.UserPhone.findByUserId(client.userId).head.flatMap { currentUserPhone ⇒
        persist.User.find(client.userId).head.flatMap { currentUser ⇒
          val filteredPhones = phones.filterNot(_.phoneNumber == currentUserPhone.number)
          val phoneNumbers = filteredPhones.map(_.phoneNumber).map(PhoneNumber.normalizeLong(_, currentUser.countryCode)).flatten.toSet
          val phonesMap = immutable.HashMap(filteredPhones.map { p ⇒ p.phoneNumber → p.name }: _*)

          val f = for {
            userPhones ← persist.UserPhone.findByNumbers(phoneNumbers)
            ignoredContactsIds ← persist.contact.UserContact.findIds_all(client.userId)
            uniquePhones = userPhones.filter(p ⇒ !ignoredContactsIds.contains(p.userId))
            usersPhones ← DBIO.sequence(uniquePhones map (p ⇒ persist.User.find(p.userId).headOption map (_.map((_, p.number))))) map (_.flatten) // TODO: #perf lots of sql queries
            userStructsSalts ← DBIO.sequence(usersPhones.map {
              case (u, phoneNumber) ⇒
                userStruct(u, phonesMap(phoneNumber), client.authId) map (us ⇒ (us, u.accessSalt))
            })
          } yield {
            val userPhoneNumbers = userPhones.map(_.number).toSet

            userStructsSalts.foldLeft((immutable.Seq.empty[(User, String)], immutable.Set.empty[Int], userPhoneNumbers)) {
              case ((userStructSalts, newContactIds, _), userStructSalt) ⇒
                (userStructSalts :+ userStructSalt,
                  newContactIds + userStructSalt._1.id,
                  userPhoneNumbers)
            }
          }

          f flatMap {
            case (userStructsSalts, newContactIds, registeredPhoneNumbers) ⇒
              actorSystem.log.debug("Phone numbers: {}, registered: {}", phoneNumbers, registeredPhoneNumbers)

              (phoneNumbers &~ registeredPhoneNumbers).foreach { phoneNumber ⇒
                actorSystem.log.debug("Inserting UnregisteredContact {} {}", phoneNumber, client.userId)

                persist.contact.UnregisteredContact.createIfNotExists(phoneNumber, client.userId)
              }

              if (userStructsSalts.nonEmpty) {
                val socialActions = newContactIds.toSeq map (id ⇒ DBIO.from(recordRelation(id, client.userId)))

                for {
                  _ ← createAllUserContacts(client.userId, userStructsSalts)
                  _ ← DBIO.sequence(socialActions)
                  seqstate ← broadcastClientUpdate(UpdateContactsAdded(newContactIds.toVector), None)
                } yield {
                  Ok(ResponseImportContacts(userStructsSalts.toVector.map(_._1), seqstate._1, seqstate._2))
                }
              } else {
                DBIO.successful(Ok(ResponseImportContacts(immutable.Vector.empty, 0, Array.empty)))
              }
          }
        }
      }
    }

    db.run(toDBIOAction(action)) // TODO: transactionally
  }

  override def jhandleGetContacts(contactsHash: String, clientData: ClientData): Future[HandlerResult[ResponseGetContacts]] = {
    val authorizedAction = requireAuth(clientData).map { implicit client ⇒
      val action = persist.contact.UserContact.findContactIdsAll(client.userId).map(hashIds).flatMap { hash ⇒
        if (contactsHash == hash) {
          DBIO.successful(Ok(ResponseGetContacts(Vector.empty[users.User], isNotChanged = true)))
        } else {
          for {
            userIdsNames ← persist.contact.UserContact.findContactIdsWithLocalNames(client.userId)
            userIds = userIdsNames.map(_._1).toSet
            users ← persist.User.findByIds(userIds)
            namesMap = immutable.Map(userIdsNames: _*)
            // TODO: #perf optimize (so much requests!)<commit_msg>fix(contacts): fixed problem with saving UnregisteredContact<commit_after>package im.actor.server.api.rpc.service.contacts

import java.security.MessageDigest

import scala.collection.immutable
import scala.concurrent._
import scala.concurrent.duration._

import akka.actor._
import akka.util.Timeout
import scodec.bits.BitVector
import slick.dbio.DBIO
import slick.driver.PostgresDriver.api._

import im.actor.api.rpc._
import im.actor.api.rpc.contacts._
import im.actor.api.rpc.misc._
import im.actor.api.rpc.users.{ UpdateUserLocalNameChanged, User }
import im.actor.server.api.util
import im.actor.server.api.util.{ ContactsUtils, PhoneNumber, UserUtils }
import im.actor.server.push.{ SeqUpdatesManager, SeqUpdatesManagerRegion }
import im.actor.server.social.{ SocialManager, SocialManagerRegion }
import im.actor.server.{ models, persist }

class ContactsServiceImpl(implicit
  val seqUpdManagerRegion: SeqUpdatesManagerRegion,
                          val socialManagerRegion: SocialManagerRegion,
                          db:                      Database,
                          actorSystem:             ActorSystem)
  extends ContactsService {

  import ContactsUtils._
  import UserUtils._
  import SocialManager._
  import SeqUpdatesManager._

  override implicit val ec: ExecutionContext = actorSystem.dispatcher
  implicit val timeout = Timeout(5.seconds)

  object Errors {
    val CantAddSelf = RpcError(401, "OWN_USER_ID", "User id cannot be equal to self.", false, None)
    val ContactAlreadyExists = RpcError(400, "CONTACT_ALREADY_EXISTS", "Contact already exists.", false, None)
    val ContactNotFound = RpcError(404, "CONTACT_NOT_FOUND", "Contact not found.", false, None)
  }

  private[service] def hashIds(ids: Seq[Int]): String = {
    val md = MessageDigest.getInstance("SHA-256")
    val uids = ids.to[immutable.SortedSet].mkString(",")
    BitVector(md.digest(uids.getBytes)).toHex
  }

  override def jhandleImportContacts(phones: Vector[PhoneToImport], emails: Vector[EmailToImport], clientData: ClientData): Future[HandlerResult[ResponseImportContacts]] = {
    val action = requireAuth(clientData).map { implicit client ⇒
      // TODO: flatten

      persist.UserPhone.findByUserId(client.userId).head.flatMap { currentUserPhone ⇒
        persist.User.find(client.userId).head.flatMap { currentUser ⇒
          val filteredPhones = phones.filterNot(_.phoneNumber == currentUserPhone.number)
          val phoneNumbers = filteredPhones.map(_.phoneNumber).map(PhoneNumber.normalizeLong(_, currentUser.countryCode)).flatten.toSet
          val phonesMap = immutable.HashMap(filteredPhones.map { p ⇒ p.phoneNumber → p.name }: _*)

          val f = for {
            userPhones ← persist.UserPhone.findByNumbers(phoneNumbers)
            ignoredContactsIds ← persist.contact.UserContact.findIds_all(client.userId)
            uniquePhones = userPhones.filter(p ⇒ !ignoredContactsIds.contains(p.userId))
            usersPhones ← DBIO.sequence(uniquePhones map (p ⇒ persist.User.find(p.userId).headOption map (_.map((_, p.number))))) map (_.flatten) // TODO: #perf lots of sql queries
            userStructsSalts ← DBIO.sequence(usersPhones.map {
              case (u, phoneNumber) ⇒
                userStruct(u, phonesMap(phoneNumber), client.authId) map (us ⇒ (us, u.accessSalt))
            })
          } yield {
            val userPhoneNumbers = userPhones.map(_.number).toSet

            userStructsSalts.foldLeft((immutable.Seq.empty[(User, String)], immutable.Set.empty[Int], userPhoneNumbers)) {
              case ((userStructSalts, newContactIds, _), userStructSalt) ⇒
                (userStructSalts :+ userStructSalt,
                  newContactIds + userStructSalt._1.id,
                  userPhoneNumbers)
            }
          }

          f flatMap {
            case (userStructsSalts, newContactIds, registeredPhoneNumbers) ⇒
              actorSystem.log.debug("Phone numbers: {}, registered: {}", phoneNumbers, registeredPhoneNumbers)

              // TODO: #perf do less queries
              val unregInsertActions = (phoneNumbers &~ registeredPhoneNumbers).toSeq map { phoneNumber ⇒
                persist.contact.UnregisteredContact.createIfNotExists(phoneNumber, client.userId)
              }

              DBIO.sequence(unregInsertActions).flatMap { _ ⇒

                if (userStructsSalts.nonEmpty) {
                  val socialActions = newContactIds.toSeq map (id ⇒ DBIO.from(recordRelation(id, client.userId)))

                  for {
                    _ ← createAllUserContacts(client.userId, userStructsSalts)
                    _ ← DBIO.sequence(socialActions)
                    seqstate ← broadcastClientUpdate(UpdateContactsAdded(newContactIds.toVector), None)
                  } yield {
                    Ok(ResponseImportContacts(userStructsSalts.toVector.map(_._1), seqstate._1, seqstate._2))
                  }
                } else {
                  DBIO.successful(Ok(ResponseImportContacts(immutable.Vector.empty, 0, Array.empty)))
                }
              }
          }
        }
      }
    }

    db.run(toDBIOAction(action)) // TODO: transactionally
  }

  override def jhandleGetContacts(contactsHash: String, clientData: ClientData): Future[HandlerResult[ResponseGetContacts]] = {
    val authorizedAction = requireAuth(clientData).map { implicit client ⇒
      val action = persist.contact.UserContact.findContactIdsAll(client.userId).map(hashIds).flatMap { hash ⇒
        if (contactsHash == hash) {
          DBIO.successful(Ok(ResponseGetContacts(Vector.empty[users.User], isNotChanged = true)))
        } else {
          for {
            userIdsNames ← persist.contact.UserContact.findContactIdsWithLocalNames(client.userId)
            userIds = userIdsNames.map(_._1).toSet
            users ← persist.User.findByIds(userIds)
            namesMap = immutable.Map(userIdsNames: _*)
            // TODO: #perf optimize (so much requests!)
{'fraction_non_alphanumeric': 0.10069385682856659, 'fraction_numerical': 0.003215434083601286, 'mean_word_length': 2.839831059129305, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2658935', 'n_tokens_mistral': 3446, 'n_tokens_neox': 3368, 'n_words': 660}