commit d190beb7bfcaf99445b43d24ccbe687e0acca7a8 Author: digimint Date: Wed Jun 19 20:55:25 2024 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e79245 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# macOS +.DS_Store + +# sbt specific +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/local-plugins.sbt +.history +.ensime +.ensime_cache/ +.sbt-scripted/ +local.sbt + +# Bloop +.bsp + +# VS Code +.vscode/ + +# Metals +.bloop/ +.metals/ +metals.sbt + +# IDEA +.idea +.idea_modules +/.worksheet/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..102c5ca --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +## sbt project compiled with Scala 3 + +### Usage + +This is a normal sbt project. You can compile code with `sbt compile`, run it with `sbt run`, and `sbt console` will start a Scala 3 REPL. + +For more information on the sbt-dotty plugin, see the +[scala3-example-project](https://github.com/scala/scala3-example-project/blob/main/README.md). diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..f4a5f1a --- /dev/null +++ b/build.sbt @@ -0,0 +1,22 @@ +val scala3Version = "3.4.1" + +lazy val root = project + .in(file(".")) + .settings( + name := "Unit-CA5 Control Program", + version := "0.1.0-SNAPSHOT", + + scalaVersion := scala3Version, + + libraryDependencies ++= Seq( + "org.scalameta" %% "munit" % "0.7.29" % Test, // Base + "org.scala-lang" %% "toolkit" % "0.1.7", + + "ch.qos.logback" % "logback-classic" % "1.5.6", // Logging + + "com.typesafe.slick" %% "slick" % "3.5.1", // Database + "com.typesafe" % "config" % "1.4.3", + "com.typesafe.slick" %% "slick-hikaricp" % "3.5.1", + "org.postgresql" % "postgresql" % "42.2.5" + ) + ) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..3c779de --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,26 @@ +# Use postgres/example user/password credentials +version: '3.9' + +services: + + db: + image: postgres + restart: always + # set shared memory limit when using docker-compose + shm_size: 128mb + # or set shared memory limit when deploy via swarm stack + #volumes: + # - type: tmpfs + # target: /dev/shm + # tmpfs: + # size: 134217728 # 128*2^20 bytes = 128Mb + environment: + POSTGRES_PASSWORD: Xo65TR6yzUJe2i4t9pQD6AfNGe23Y8ww8CtW7aADfrLLivSsuYpB36oyXXBLH443 + ports: + - 5432:5432 + + adminer: + image: adminer + restart: always + ports: + - 8080:8080 \ No newline at end of file diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..04267b1 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/src/main/resources/.gitignore b/src/main/resources/.gitignore new file mode 100644 index 0000000..13c5cd7 --- /dev/null +++ b/src/main/resources/.gitignore @@ -0,0 +1,2 @@ +# Prevent unintentional commit of secrets +application.conf \ No newline at end of file diff --git a/src/main/resources/application.conf.skel b/src/main/resources/application.conf.skel new file mode 100644 index 0000000..e44347d --- /dev/null +++ b/src/main/resources/application.conf.skel @@ -0,0 +1,17 @@ +db_conf = { + connectionPool = "HikariCP" + dataSourceClass = "org.postgresql.ds.PGSimpleDataSource" + properties = { + serverName = "" + portNumber = "" + databaseName = "" + user = "" + password = "" + } + numThreads = 1 +} + +twitch_conf = { + client_id = "" + client_secret = "" +} \ No newline at end of file diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala new file mode 100644 index 0000000..4c7ddbd --- /dev/null +++ b/src/main/scala/Main.scala @@ -0,0 +1,86 @@ +package unit_ca5 + +import slick.jdbc.PostgresProfile.api._ +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import com.typesafe.config.ConfigFactory +import com.typesafe.config.Config +import java.time.Instant + +@main def hello(): Unit = + println("Hello world!") + println(msg) + + val conf: Config = ConfigFactory.load("application.conf") + val test_key: String = conf.getString("test") + + println(s"Test config key: ${test_key}") + + Await.result(DBConnection.doSetup, Duration.Inf) + println("Database initialized") + + Await.result(DBConnection.doTestInserts, Duration.Inf) + println("Test values inserted") + +def msg = "I was compiled by Scala 3. :)" + +class UserTokens(tag: Tag) extends Table[(Int, String, String, String, Instant, Int)](tag, "USER_TOKENS"): + def id = column[Int]("TKN_ID", O.PrimaryKey, O.AutoInc) + def userId = column[String]("USER_ID") + def accessToken = column[String]("ACCESS_TKN") + def refreshToken = column[String]("REFRESH_TKN") + def expires = column[Instant]("EXPIRES") + + // Compact representation of the scopes granted to this token. If a version + // update changes the set of required scopes, the "current" SCOPES_VERSION + // is incremented. This then gets mapped to a set of actual scopes, which + // other functions can check. + def scopes_version = column[Int]("SCOPES_VERSION") + + def * = (id, userId, accessToken, refreshToken, expires, scopes_version) + + def user = foreignKey("USER_FK", userId, authUsers)(_.userId) + +val userTokens = TableQuery[UserTokens] + +class AuthUsers(tag: Tag) extends Table[(String, String)](tag, "USERS"): + def userId = column[String]("USER_ID", O.PrimaryKey) + def name = column[String]("TWITCH_USERNAME") + + def * = (userId, name) + +val authUsers = TableQuery[AuthUsers] + +object DBConnection{ + val db = Database.forConfig("db_conf") + + val setup = DBIO.seq( + (authUsers.schema ++ userTokens.schema).createIfNotExists + ) + + def doSetup = + db.run(setup) + + def doTestInserts = + val testInserts = DBIO.seq( + authUsers ++= Seq( + ("a", "digimint"), + ("b", "PancakeDragoness") + ), + + userTokens ++= Seq( + (1, "a", "coolAccessToken", "coolRefreshToken", Instant.now, 1), + (2, "b", "hotAccessToken", "hotRefreshToken", Instant.now, 1) + ) + ) + + db.run(testInserts) + // val userInserts: DBIO[Option[Int]] = authUsers ++= Seq ( + // ("a", "digimint"), + // ("b", "PancakeDragoness") + // ) + + // val tokensInserts: DBIO[Option[Int]] = userTokens ++= Seq ( + // () + // ) +} \ No newline at end of file diff --git a/src/main/scala/db/schema/ScopesVersion.scala b/src/main/scala/db/schema/ScopesVersion.scala new file mode 100644 index 0000000..cf813fb --- /dev/null +++ b/src/main/scala/db/schema/ScopesVersion.scala @@ -0,0 +1,48 @@ +package unit_ca5.db.schema + +import unit_ca5.twitch.TokenScope + +trait ScopesVersion: + val scopes: List[TokenScope] + val version: Int + + def toScopes(): List[TokenScope] = scopes + + def toInt(): Int = version + + def matches(version: Int): Boolean = + this.version == version + + def matches(scopes: List[TokenScope]): Boolean = + this.scopes == scopes + + +object ScopesVersion: + val allScopesVersions: List[ScopesVersion] = List( + scopesVersion1 + ) + + // Construct from a list of scopes + def apply(scopes: List[TokenScope]): Option[ScopesVersion] = + allScopesVersions.find( + (candidate: ScopesVersion) => + candidate.matches(scopes) + ) + + // Construct from a version number + def apply(version: Int): Option[ScopesVersion] = + allScopesVersions.find( + (candidate: ScopesVersion) => + candidate.matches(version) + ) + +val scopesVersion1: ScopesVersion = + new ScopesVersion: + val scopes = List( + TokenScope.UserReadChat, + TokenScope.BitsRead, + TokenScope.ChannelManageRaids + ) + val version = 1 + + \ No newline at end of file diff --git a/src/main/scala/db/schema/UserToken.scala b/src/main/scala/db/schema/UserToken.scala new file mode 100644 index 0000000..ecbc47b --- /dev/null +++ b/src/main/scala/db/schema/UserToken.scala @@ -0,0 +1,22 @@ +package unit_ca5.db.schema + +import slick.jdbc.PostgresProfile.api._ + +import unit_ca5.twitch.UserAuthenticationCredential +import unit_ca5.twitch.TokenScope +import java.time.Instant + +class UserAuthenticationCredentialTable(tag: Tag) extends Table[UserAuthenticationCredential](tag, "USER_TOKENS"): + def id = column[Int]("TKN_ID", O.PrimaryKey, O.AutoInc) + def userId = column[String]("USER_ID") + def accessToken = column[String]("ACCESS_TKN") + def refreshToken = column[String]("REFRESH_TKN") + def expires = column[Instant]("EXPIRES") + + // Compact representation of the scopes granted to this token. If a version + // update changes the set of required scopes, the "current" SCOPES_VERSION + // is incremented. This then gets mapped to a set of actual scopes, which + // other functions can check. + def scopes_version = column[Int]("SCOPES_VERSION") + + def * = ??? \ No newline at end of file diff --git a/src/main/scala/twitch/api/TokenScope.scala b/src/main/scala/twitch/api/TokenScope.scala new file mode 100644 index 0000000..286fbe1 --- /dev/null +++ b/src/main/scala/twitch/api/TokenScope.scala @@ -0,0 +1,84 @@ +package unit_ca5.twitch + +enum TokenScope: + // API Scopes + case AnalyticsReadExtensions + case AnalyticsReadGames + + case BitsRead + + case ChannelManageAds + case ChannelReadAds + case ChannelManageBroadcast + case ChannelReadCharity + case ChannelEditCommercial + case ChannelReadEditors + case ChannelManageExtensions + case ChannelReadGoals + case ChannelReadGuestStar + case ChannelManageGuestStar + case ChannelReadHypeTrain + case ChannelManageModerators + case ChannelReadPolls + case ChannelManagePolls + case ChannelReadPredictions + case ChannelManagePredictions + case ChannelManageRaids + case ChannelReadRedemptions + case ChannelManageRedemptions + case ChannelManageSchedule + case ChannelReadStreamKey + case ChannelReadSubscriptions + case ChannelManageVideos + case ChannelReadVIPs + case ChannelManageVIPs + + case ClipsEdit + + case ModerationRead + + case ModeratorManageAnnouncement + case ModeratorManageAutomod + case ModeratorReadAutomodSettings + case ModeratorManageAutomodSettings + case ModeratorManageBannedUsers + case ModeratorReadBlockedTerms + case ModeratorManageBlockedTerms + case ModeratorManageChatMessages + case ModeratorReadChatters + case ModeratorReadFollowers + case ModeratorReadGuestStar + case ModeratorManageGuestStar + case ModeratorReadShieldMode + case ModeratorManageShieldMode + case ModeratorReadShoutouts + case ModeratorManageShoutouts + case ModeratorReadUnbanRequests + case ModeratorManageUnbanRequests + + case UserEdit + case UserEditFollows + case UserReadBlockedUsers + case UserManageBlockedUsers + case UserReadBroadcast + case UserManageChatColor + case UserReadEmail + case UserReadEmotes + case UserReadFollows + case UserReadModeratedChannels + case UserReadSubscriptions + case UserManageWhispers + + // Chat and PubSub scopes + case ChannelBot + case ChannelModerate + + case ChatEdit + case ChatRead + + case UserBot + case UserReadChat + case UserWriteChat + + case WhispersRead + case WhispersEdit \ No newline at end of file diff --git a/src/main/scala/twitch/api/UserAuthenticationCredential.scala b/src/main/scala/twitch/api/UserAuthenticationCredential.scala new file mode 100644 index 0000000..e171b4b --- /dev/null +++ b/src/main/scala/twitch/api/UserAuthenticationCredential.scala @@ -0,0 +1,25 @@ +package unit_ca5.twitch + +import java.time.Instant + +import unit_ca5.twitch.TokenScope + +type TwitchUID = String +type AccessToken = String +type RefreshToken = String + +case class UserAuthenticationCredential( + userId: TwitchUID, + accessToken: AccessToken, + refreshToken: RefreshToken, + expires: Instant, + scopes: List[TokenScope] +): + def is_expired(now: Instant): Boolean = + now.isAfter(expires) + + def supports(scope: TokenScope): Boolean = + scopes.contains(scope) + + def supportsAll(scopeList: List[TokenScope]): Boolean = + \ No newline at end of file diff --git a/src/test/scala/MySuite.scala b/src/test/scala/MySuite.scala new file mode 100644 index 0000000..621784d --- /dev/null +++ b/src/test/scala/MySuite.scala @@ -0,0 +1,9 @@ +// For more information on writing tests, see +// https://scalameta.org/munit/docs/getting-started.html +class MySuite extends munit.FunSuite { + test("example test that succeeds") { + val obtained = 42 + val expected = 42 + assertEquals(obtained, expected) + } +}