MiniDigger/Hangar

View on GitHub
orePlayCommon/app/db/impl/access/OrganizationBase.scala

Summary

Maintainability
A
3 hrs
Test Coverage
package db.impl.access

import scala.language.higherKinds

import ore.OreConfig
import ore.auth.SpongeAuthApi
import ore.data.user.notification.NotificationType
import ore.db.access.ModelView
import ore.db.{DbRef, Model, ModelService, ObjId}
import ore.models.organization.Organization
import ore.models.user.role.OrganizationUserRole
import ore.models.user.{Notification, User}
import ore.permission.role.Role
import ore.util.{OreMDC, StringUtils}
import util.syntax._

import cats.Parallel
import cats.data.{EitherT, NonEmptyList, OptionT}
import cats.effect.Sync
import cats.syntax.all._
import com.typesafe.scalalogging

trait OrganizationBase[F[_]] {

  /**
    * Creates a new [[Organization]]. This method creates a new user on the
    * forums to represent the Organization.
    *
    * @param name     Organization name
    * @param ownerId  User ID of the organization owner
    * @return         New organization if successful, None otherwise
    */
  def create(
      name: String,
      ownerId: DbRef[User],
      members: Set[OrganizationUserRole]
  )(implicit mdc: OreMDC): EitherT[F, List[String], Model[Organization]]

  /**
    * Returns an [[Organization]] with the specified name if it exists.
    *
    * @param name Organization name
    * @return     Organization with name if exists, None otherwise
    */
  def withName(name: String): OptionT[F, Model[Organization]]
}

object OrganizationBase {

  /**
    * Default live implementation of [[OrganizationBase]]
    */
  class OrganizationBaseF[F[_], G[_]](
      implicit val service: ModelService[F],
      config: OreConfig,
      auth: SpongeAuthApi[F],
      F: Sync[F],
      par: Parallel[F, G],
      users: UserBase[F]
  ) extends OrganizationBase[F] {

    private val Logger    = scalalogging.Logger("Organizations")
    private val MDCLogger = scalalogging.Logger.takingImplicit[OreMDC](Logger.underlying)

    def create(
        name: String,
        ownerId: DbRef[User],
        members: Set[OrganizationUserRole]
    )(implicit mdc: OreMDC): EitherT[F, List[String], Model[Organization]] = {
      import cats.instances.vector._
      val logging = F.delay {
        MDCLogger.debug("Creating Organization...")
        MDCLogger.debug("Name     : " + name)
        MDCLogger.debug("Owner ID : " + ownerId)
        MDCLogger.debug("Members  : " + members.size)

        // Create the organization as a User on SpongeAuth. This will reserve the
        // name so that no new users or organizations can create an account with
        // that name. We will give the organization a dummy email for continuity.
        // By default we use "<org>@ore.spongepowered.org".
        MDCLogger.debug("Creating on SpongeAuth...")
      }

      // Replace all invalid characters to not throw invalid email error when trying to create org with invalid username
      val dummyEmail   = name.replaceAll("[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]", "") + '@' + config.ore.orgs.dummyEmailDomain
      val spongeResult = EitherT.right[List[String]](logging) *> auth.createDummyUser(name, dummyEmail)

      // Check for error
      spongeResult
        .leftMap { err =>
          MDCLogger.debug("<FAILURE> " + err)
          err
        }
        .semiflatMap { spongeUser =>
          MDCLogger.debug("<SUCCESS> " + spongeUser)
          // Next we will create the Organization on Ore itself. This contains a
          // reference to the Sponge user ID, the organization's username and a
          // reference to the User owner of the organization.
          MDCLogger.info("Creating on Ore...")
          service.insert(Organization(id = ObjId(spongeUser.id), username = name, ownerId = ownerId))
        }
        .semiflatMap { org =>
          // Every organization model has a regular User companion. Organizations
          // are just normal users with additional information. Adding the
          // Organization global role signifies that this User is an Organization
          // and should be treated as such.
          for {
            userOrg <- org.toUser.getOrElseF(F.raiseError(new IllegalStateException("User not created")))
            _       <- userOrg.globalRoles.addAssoc(Role.Organization.toDbRole.id.value)
            _ <- // Add the owner
            org.memberships.addRole(org)(
              ownerId,
              OrganizationUserRole(
                userId = ownerId,
                organizationId = org.id,
                role = Role.OrganizationOwner,
                isAccepted = true
              )
            )
            _ <- {
              // Invite the User members that the owner selected during creation.
              MDCLogger.debug("Inviting members...")

              members.toVector.parTraverse { role =>
                // TODO remove role.user db access we really only need the userid we already have for notifications
                org.memberships.addRole(org)(role.userId, role.copy(organizationId = org.id)).flatMap { _ =>
                  service.insert(
                    Notification(
                      userId = role.userId,
                      originId = Some(org.id),
                      notificationType = NotificationType.OrganizationInvite,
                      messageArgs = NonEmptyList.of("notification.organization.invite", role.role.title, org.username)
                    )
                  )
                }
              }
            }
          } yield {
            MDCLogger.debug("<SUCCESS> " + org)
            org
          }
        }
    }

    /**
      * Returns an [[Organization]] with the specified name if it exists.
      *
      * @param name Organization name
      * @return     Organization with name if exists, None otherwise
      */
    def withName(name: String): OptionT[F, Model[Organization]] =
      ModelView.now(Organization).find(StringUtils.equalsIgnoreCase(_.name, name))

  }

  def apply[F[_]](implicit organizationBase: OrganizationBase[F]): OrganizationBase[F] = organizationBase
}