Skip to content

Commit

Permalink
Release/2.0.0 (#120)
Browse files Browse the repository at this point in the history
feat: support modern Slack apps

itiviti/simple-slack-api replaced with official library from Slack team com.slack.api.bolt-socket-mode

Scala updated up to 2.13

Scalafmt enabled and all code was reformatted.
  • Loading branch information
alexkvak authored Mar 19, 2021
1 parent 674abc8 commit a2633d9
Show file tree
Hide file tree
Showing 37 changed files with 1,719 additions and 936 deletions.
1 change: 1 addition & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version = 2.7.5
13 changes: 4 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ sources with `mvn package`.
Next upload `target/slackIntegration.zip` to TeamCity `data/plugins/` folder (restart is needed).

Create Slack App:
* Open [Create classic app](https://api.slack.com/apps?new_classic_app=1) form and fill it
* Go to **App Home**. Create Bot with **Add Legacy Bot User** button (fill both fields correctly)
* Go to **OAuth & Permissions**. Add scope **bot** in **Scopes** section. Then click **Install App in Workspace**
* Now copy **Bot User OAuth Access Token**.
* Open [Create app](https://api.slack.com/apps) form and fill it
* Go to **OAuth & Permissions**. Add following scopes in **Scopes** section: `channels:read`, `chat:write`, `chat:write.public`, `im:write`, `users:read`, `users:read.email`. If you plan to change sender name, add also `chat:write.customize` scope.
* Click **Install App in Workspace**
* Now copy **Bot User OAuth Token**.

Paste this token into **Administration -> Slack -> OAuth Access Token** field.

Expand Down Expand Up @@ -150,11 +150,6 @@ The message is prepended by Emoji ✅, ⛔ or ⚪ for successful, failed and oth


## Troubleshooting <a name="troubleshooting"></a>
**Q:** I followed all the instructions, but I get error message `not_allowed_token_type` when I try to save my Bot token into TeamCity!

**A:** This plugin does not yet support the new Slack detailed OAuth scopes. When trying to save or use a token created for a Bot User using the new scopes, the Slack API will return that error message. When creating a new Slack App for this integration, do NOT opt into using the updated/beta scopes. If you have already opted in, you will need to create a new Slack App - there is no way to downgrade at this time. The only supported scope is the classic `bot` scope.

![Beta Slack bot scopes](_doc/slack-beta-bot-scopes.png)

**Q:** I checked the option to send private messages and added the `{mention}` placeholder to the message, but neither the message was send to the slack user nor the name was mentioned in the slack message!

Expand Down
Binary file removed _doc/slack-beta-bot-scopes.png
Binary file not shown.
2 changes: 1 addition & 1 deletion build/plugin-assembly.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<files>
<file>
<source>target/teamcity-plugin.xml</source>
<outputDirectory>/</outputDirectory>
<outputDirectory/>
</file>
</files>
<moduleSets>
Expand Down
2 changes: 1 addition & 1 deletion build/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<packaging>pom</packaging>
<properties>
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
<teamcity.slackIntegration.plugin.version>1.8.1</teamcity.slackIntegration.plugin.version>
<teamcity.slackIntegration.plugin.version>2.0.0</teamcity.slackIntegration.plugin.version>
<teamcity.slackIntegration.plugin.vendorName>Alex Kvak</teamcity.slackIntegration.plugin.vendorName>
<teamcity.slackIntegration.plugin.vendorUrl>https://github.com/alexkvak</teamcity.slackIntegration.plugin.vendorUrl>
</properties>
Expand Down
11 changes: 4 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,14 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<scalaVersion>2.12.1</scalaVersion>
</configuration>
<version>4.4.1</version>
<executions>
<execution>
<id>scala-compile-first</id>
Expand Down Expand Up @@ -67,7 +64,7 @@
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0</version>
<version>2.0.2</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
Expand Down
35 changes: 23 additions & 12 deletions slackIntegration-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
</parent>
<artifactId>slackIntegration-server</artifactId>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>

<dependency>
Expand All @@ -21,7 +26,7 @@
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.1</version>
<version>2.13.5</version>
</dependency>

<dependency>
Expand All @@ -34,22 +39,28 @@

<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.12</artifactId>
<version>3.0.3</version>
<artifactId>scalatest_2.13</artifactId>
<version>3.2.6</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.scalamock</groupId>
<artifactId>scalamock-scalatest-support_2.12</artifactId>
<version>3.6.0</version>
<artifactId>scalamock_2.13</artifactId>
<version>5.1.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.ullink.slack</groupId>
<artifactId>simpleslackapi</artifactId>
<version>1.0.0</version>
<groupId>com.slack.api</groupId>
<artifactId>bolt-socket-mode</artifactId>
<version>1.6.2</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
Expand All @@ -68,14 +79,14 @@

<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-native_2.12</artifactId>
<version>3.5.1</version>
<artifactId>json4s-native_2.13</artifactId>
<version>3.6.11</version>
</dependency>

<dependency>
<groupId>org.json4s</groupId>
<artifactId>json4s-ext_2.12</artifactId>
<version>3.5.1</version>
<artifactId>json4s-ext_2.13</artifactId>
<version>3.6.11</version>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
<label for="senderName">Sender name</label>
</td><td>
<input type="text" id="senderName" name="senderName" value="${senderName}" class="longField">
<div class="smallNoteAttention">
Send message as other user. <br />
Please make sure that you granted the scope <code>chat:write.customize</code>
</div>
</td>
</tr>
<tr class="js-toggable">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ class ConfigManager(paths: ServerPaths) {
override def alwaysEscapeUnicode: Boolean = true
} + new EnumNameSerializer(BuildSettingFlag)

private def configFile = new File(s"${paths.getConfigDir}/slackIntegration.json")
private def configFile = new File(
s"${paths.getConfigDir}/slackIntegration.json"
)

private def backupFile = new File(s"${paths.getConfigDir}/slackIntegration.json.bak")
private def backupFile = new File(
s"${paths.getConfigDir}/slackIntegration.json.bak"
)

private[teamcity] var config: Option[Config] = readConfig

Expand All @@ -31,18 +35,22 @@ class ConfigManager(paths: ServerPaths) {

def oauthKey: Option[String] = config.map(_.oauthKey)
def publicUrl: Option[String] = config.flatMap(_.publicUrl)
def senderName: Option[String] = config.flatMap(_.senderName).filter(_.nonEmpty)
def senderName: Option[String] =
config.flatMap(_.senderName).filter(_.nonEmpty)
def enabled: Option[Boolean] = config.flatMap(_.enabled)
def personalEnabled: Option[Boolean] = config.flatMap(_.personalEnabled)
def sendAsAttachment: Option[Boolean] = config.flatMap(_.sendAsAttachment)

def allBuildSettingList: BuildSettings = config.map(_.buildSettings).getOrElse(Map.empty)
def allBuildSettingList: BuildSettings =
config.map(_.buildSettings).getOrElse(Map.empty)

def buildSettingList(buildTypeId: String): BuildSettings = allBuildSettingList.filter {
case (_, setting) setting.buildTypeId == buildTypeId
}
def buildSettingList(buildTypeId: String): BuildSettings =
allBuildSettingList.filter { case (_, setting) =>
setting.buildTypeId == buildTypeId
}

def buildSetting(id: String): Option[BuildSetting] = allBuildSettingList.get(id)
def buildSetting(id: String): Option[BuildSetting] =
allBuildSettingList.get(id)

private def updateAndPersist(newConfig: Config): Boolean = {
backup()
Expand All @@ -58,78 +66,100 @@ class ConfigManager(paths: ServerPaths) {
val out = new PrintWriter(file, "UTF-8")
try {
writePretty(config, out)
}
finally {
} finally {
out.close()
}

true
}

def updateBuildSetting(setting: BuildSetting, keyOption: Option[String]): Option[Boolean] = config.map { c
def updateBuildSetting(
setting: BuildSetting,
keyOption: Option[String]
): Option[Boolean] = config.map { c =>
val newSettings = keyOption match {
case Some(key)
case Some(key) =>
c.buildSettings.updated(key, setting)
case _
c.buildSettings + (nextKey(allBuildSettingList) setting)
case _ =>
c.buildSettings + (nextKey(allBuildSettingList) -> setting)
}

updateAndPersist(c.copy(buildSettings = newSettings))
}

def update(authKey: String, pubUrl: String, personalEnabled: Boolean, enabled: Boolean, sender: String, sendAsAttachment: Boolean): Boolean = config match {
case Some(c)
updateAndPersist(c.copy(
authKey, publicUrl = Some(pubUrl), personalEnabled = Some(personalEnabled),
enabled = Some(enabled), senderName = Some(sender),
sendAsAttachment = Some(sendAsAttachment)
))
case None
updateAndPersist(Config(
authKey, publicUrl = Some(pubUrl), personalEnabled = Some(personalEnabled),
enabled = Some(enabled), senderName = Some(sender),
sendAsAttachment = Some(sendAsAttachment)
))
def update(
authKey: String,
pubUrl: String,
personalEnabled: Boolean,
enabled: Boolean,
sender: String,
sendAsAttachment: Boolean
): Boolean = config match {
case Some(c) =>
updateAndPersist(
c.copy(
authKey,
publicUrl = Some(pubUrl),
personalEnabled = Some(personalEnabled),
enabled = Some(enabled),
senderName = Some(sender),
sendAsAttachment = Some(sendAsAttachment)
)
)
case None =>
updateAndPersist(
Config(
authKey,
publicUrl = Some(pubUrl),
personalEnabled = Some(personalEnabled),
enabled = Some(enabled),
senderName = Some(sender),
sendAsAttachment = Some(sendAsAttachment)
)
)
}

def removeBuildSetting(key: String): Option[Boolean] = config.map { c
def removeBuildSetting(key: String): Option[Boolean] = config.map { c =>
updateAndPersist(c.copy(buildSettings = c.buildSettings - key))
}

def details: Map[String, Option[String]] = Map(
"oauthKey" oauthKey,
"publicUrl" publicUrl,
"senderName" senderName,
"enabled" enabled.filter(x x).map(_ "1"),
"personalEnabled" personalEnabled.filter(x x).map(_ "1"),
"sendAsAttachment" sendAsAttachment.filter(x x).map(_ "1")
"oauthKey" -> oauthKey,
"publicUrl" -> publicUrl,
"senderName" -> senderName,
"enabled" -> enabled.filter(x => x).map(_ => "1"),
"personalEnabled" -> personalEnabled.filter(x => x).map(_ => "1"),
"sendAsAttachment" -> sendAsAttachment.filter(x => x).map(_ => "1")
)

def isAvailable: Boolean = config.exists(c c.enabled.exists(b b) && c.oauthKey.length > 0)
def isAvailable: Boolean =
config.exists(c => c.enabled.exists(b => b) && c.oauthKey.nonEmpty)
}

object ConfigManager {

object BuildSettingFlag extends Enumeration {
type BuildSettingFlag = Value

val success, failureToSuccess, failure, successToFailure, canceled, started, queued = Value
val success, failureToSuccess, failure, successToFailure, canceled, started,
queued = Value
}

import BuildSettingFlag._

type BuildSettings = Map[String, BuildSetting]

case class BuildSetting(buildTypeId: String,
branchMask: String,
slackChannel: String,
messageTemplate: String,
flags: Set[BuildSettingFlag] = Set.empty,
artifactsMask: String = "",
deepLookup: Boolean = false,
notifyCommitter: Boolean = false,
maxVcsChanges: Int = BuildSetting.defaultMaxVCSChanges
) {
case class BuildSetting(
buildTypeId: String,
branchMask: String,
slackChannel: String,
messageTemplate: String,
flags: Set[BuildSettingFlag] = Set.empty,
artifactsMask: String = "",
deepLookup: Boolean = false,
notifyCommitter: Boolean = false,
maxVcsChanges: Int = BuildSetting.defaultMaxVCSChanges
) {
// Getters for JSP
lazy val getBranchMask: String = branchMask
lazy val getSlackChannel: String = slackChannel
Expand All @@ -140,15 +170,16 @@ object ConfigManager {
lazy val getMaxVcsChanges: Int = maxVcsChanges
// Flags
lazy val getSuccess: Boolean = flags.contains(BuildSettingFlag.success)
lazy val getFailureToSuccess: Boolean = flags.contains(BuildSettingFlag.failureToSuccess)
lazy val getFailureToSuccess: Boolean =
flags.contains(BuildSettingFlag.failureToSuccess)
lazy val getFail: Boolean = flags.contains(BuildSettingFlag.failure)
lazy val getSuccessToFailure: Boolean = flags.contains(BuildSettingFlag.successToFailure)
lazy val getSuccessToFailure: Boolean =
flags.contains(BuildSettingFlag.successToFailure)
lazy val getCanceled: Boolean = flags.contains(BuildSettingFlag.canceled)
lazy val getStarted: Boolean = flags.contains(BuildSettingFlag.started)
lazy val getQueued: Boolean = flags.contains(BuildSettingFlag.queued)

/**
* Removes success flag if failureToSuccess is set
/** Removes success flag if failureToSuccess is set
* and failure flag if successToFailure is set
*
* @return
Expand All @@ -169,14 +200,15 @@ object ConfigManager {
lazy val defaultMaxVCSChanges = 5
}

case class Config(oauthKey: String,
buildSettings: BuildSettings = Map.empty,
publicUrl: Option[String] = None,
personalEnabled: Option[Boolean] = Some(true),
sendAsAttachment: Option[Boolean] = Some(true),
enabled: Option[Boolean] = Some(true),
senderName: Option[String] = None
)
case class Config(
oauthKey: String,
buildSettings: BuildSettings = Map.empty,
publicUrl: Option[String] = None,
personalEnabled: Option[Boolean] = Some(true),
sendAsAttachment: Option[Boolean] = Some(true),
enabled: Option[Boolean] = Some(true),
senderName: Option[String] = None
)

@annotation.tailrec
private def nextKey(map: BuildSettings): String = {
Expand Down
Loading

0 comments on commit a2633d9

Please sign in to comment.