All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mchange.feedletter.AbstractMain.scala Maven / Gradle / Ivy

package com.mchange.feedletter

import zio.*
import java.lang.System

import com.monovore.decline.*
import cats.implicits.* // for mapN
import cats.data.{NonEmptyList,Validated,ValidatedNel}

import java.nio.file.{Path as JPath}
import java.time.ZoneId
import java.util.{Properties, Map as JMap}
import javax.sql.DataSource

import com.mchange.mailutil.*

import MLevel.*

trait AbstractMain extends SelfLogging:
  object CommonOpts:
    val AnyDestination : Opts[Destination] =
      val email =
        val general = Opts.option[String]("e-mail",help="The e-mail address to subscribe.",metavar="address")
        val displayName = Opts.option[String]("display-name",help="A display name to wrap around the e-mail address.",metavar="name").orNone
        ( general, displayName ) mapN: (g, dn) =>
          val tmp = Destination.Email( Smtp.Address.parseSingle( g ) )
          dn.fold(tmp)( n => tmp.copy( displayNamePart = Some(n) ) )
      val sms = Opts.option[String]("sms",help="The number to which messages should be sent.",metavar="number").map( n => Destination.Sms(number=n) )
      val mastodon =
        val instanceName = Opts.option[String]("masto-instance-name",help="A private name for this Mastodon instance.",metavar="name")
        val instanceUrl = Opts.option[String]("masto-instance-url",help="The URL of the Mastodon instance",metavar="url")
        ( instanceName, instanceUrl ) mapN: (in, iu) =>
          Destination.Mastodon( name = in, instanceUrl = iu )
      email orElse sms orElse mastodon
    val AwaitStabilizationMinutes =
      val help = "Period (in minutes) over which an item should not have changed before it is considered stable and can be notified."
      Opts.option[Int]("await-stabilization-minutes", help=help, metavar="minutes")
    val ExtraParams : Opts[Map[String,String]] =
      def validate( strings : List[String] ) : ValidatedNel[String,List[Tuple2[String,String]]] =
        strings.map{ s =>
          s.split(":", 2) match
            case Array(key, value) => Validated.valid(Tuple2(key, value))
            case _ => Validated.invalidNel(s"Invalid key:value pair: ${s}")
        }.sequence
      Opts.options[String]("extra-param", "An extra parameter your notification renderers might use.", metavar = "key:value")
        .map( _.toList)
        .withDefault(Nil)
        .mapValidated( validate )
        .map( Map.from )
    val ComposeUntemplateName =
      val help = "Fully qualified name of untemplate that will render notifications."
      Opts.option[String]("compose-untemplate",help=help,metavar="fully-qualified-name").orNone
    val ConfirmUntemplateName =
      val help = "Fully qualified name of untemplate that will ask for e-mail confirmations."
      Opts.option[String]("confirm-untemplate",help=help,metavar="fully-qualified-name").orNone
    val MaxDelayMinutes =
      val help = "Notwithstanding other settings, maximum period past which an item should be notified, regardless of its stability."
      Opts.option[Int]("max-delay-minutes", help=help, metavar="minutes")
    val MinDelayMinutes =
      val help = "Minimum wait (in miunutes) before a newly encountered item can be notified."
      Opts.option[Int]("min-delay-minutes", help=help, metavar="minutes")
    val RecheckEveryMinutes =
      val help = "Delay between refreshes of feeds, and redetermining items' availability for notification."
      Opts.option[Int]("recheck-every-minutes", help=help, metavar="minutes")
    val RemovalNotificationUntemplateName =
      val help = "Fully qualified name of untemplate that be mailed to users upon unsubscription."
      Opts.option[String]("removal-notification-untemplate",help=help,metavar="fully-qualified-name").orNone
    val StatusChangeUntemplateName =
      val help = "Fully qualified name of untemplate that will render results of GET request to the API."
      Opts.option[String]("status-change-untemplate",help=help,metavar="fully-qualified-name").orNone
    val Secrets = 
      val help = "Path to properties file containing SMTP, postgres, c3p0, and other configuration details."
      val opt  = Opts.option[JPath]("secrets",help=help,metavar="propsfile")
      val env  = Opts.env[JPath]("FEEDLETTER_SECRETS", help=help)
      (opt orElse env).orNone
    val SubscribableNameDefined =
        val help = "The name of an already-defined subscribable."
        Opts.option[String]("subscribable-name",help=help,metavar="name").map( SubscribableName.apply )
    val TimeZone = Opts.option[String]("time-zone",help="ID of a time zone for determining the beginning and end of the period.",metavar="id").map( ZoneId.of ) 
  end CommonOpts

  val LayerDataSource : ZLayer[AppSetup, Throwable, DataSource] =
    import com.mchange.v2.beans.BeansUtils
    import com.mchange.v2.c3p0.ComboPooledDataSource
    import scala.jdk.CollectionConverters.*

    def createDataSource( appSetup : AppSetup ) : DataSource =
      val c3p0PropsJMap =
        appSetup.secrets
          .filter( (k, _) => k.startsWith("c3p0.") )
          .map( (k, v) => (k.substring(5), v) )
          .asJava

      val nascent = new ComboPooledDataSource()
      BeansUtils.overwriteAccessiblePropertiesFromMap(
        c3p0PropsJMap, // sourceMap
        nascent,       // destination bean
        true,          // skip nulls
        null,          // props to ignore, null means none
        true,          // do coerce strings
        null,          // null means log to default (WARNING) level if can't write
        null,          // null means log to default (WARNING) level if can't coerce
        false          // don't die on failures, continue
      )
      appSetup.secrets.get( SecretsKey.FeedletterJdbcUrl ).foreach( nascent.setJdbcUrl )
      appSetup.secrets.get( SecretsKey.FeedletterJdbcUser ).foreach( nascent.setUser )
      appSetup.secrets.get( SecretsKey.FeedletterJdbcPassword ).foreach( nascent.setPassword )
      nascent

    ZLayer.fromFunction( createDataSource _ )

  def baseCommand : Command[(Option[JPath],CommandConfig)]

  def main( args : Array[String] ) : Unit =
    try
      baseCommand.parse(args.toIndexedSeq, sys.env) match
        case Left(help) =>
          println(help)
          System.exit(1)
        case Right( ( mbSecrets : Option[JPath], cc : CommandConfig ) ) =>
          val task =
            cc.zcommand.provide( AppSetup.live(mbSecrets), LayerDataSource )
              .tapError( t => SEVERE.zlog( "Application failed with an Exception.", t ) )
              .tapDefect( cause => SEVERE.zlog( "Application failed with cause: " + cause ) )
          Unsafe.unsafely:
            Runtime.default.unsafe.run(task).getOrThrow()
    catch
      case t : Throwable =>
        System.err.println("Function main(...) is terminating with an Exception:")
        t.printStackTrace()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy