Initial commit.

This commit is contained in:
Erik C. Thauvin 2019-09-18 05:16:01 -07:00
commit 271568e674
20 changed files with 1426 additions and 0 deletions

2
.editorconfig Normal file
View file

@ -0,0 +1,2 @@
[*]
insert_final_newline=true

5
.gitattributes vendored Normal file
View file

@ -0,0 +1,5 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# batch files are specific to windows and always crlf
*.bat eol=crlf

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
**/*.class
**/.idea/$CACHE_FILE$
**/.idea/$PRODUCT_WORKSPACE_FILE$
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.local.xml
**/.idea/**/dataSources/
**/.idea/**/dbnavigator.xml
**/.idea/**/dictionaries
**/.idea/**/dynamic.xml
**/.idea/**/gradle.xml
**/.idea/**/libraries
**/.idea/**/shelf
**/.idea/**/sqlDataSources.xml
**/.idea/**/tasks.xml
**/.idea/**/uiDesigner.xml
**/.idea/**/usage.statistics.xml
**/.idea/**/workspace.xml
*.code-workspace
*.iws
*.sublime-*
.DS_Store
.classpath
.gradle
.kobalt
.nb-gradle
.project
.settings
.vscode/*
/bin
/build
/deploy
/dist
/gen
/gradle.properties
/local.properties
/out
/proguard-project.txt
/project.properties
/target
/test-output
Thumbs.db
ehthumbs.db
kobaltBuild
kobaltw*-test

16
.idea/checkstyle-idea.xml generated Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA">
<option name="configuration">
<map>
<entry key="checkstyle-version" value="8.24" />
<entry key="copy-libs" value="true" />
<entry key="location-0" value="BUNDLED:(bundled):Sun Checks" />
<entry key="location-1" value="BUNDLED:(bundled):Google Checks" />
<entry key="scan-before-checkin" value="false" />
<entry key="scanscope" value="JavaOnly" />
<entry key="suppress-errors" value="false" />
</map>
</option>
</component>
</project>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Erik's Code Style" />
</state>
</component>

View file

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="&amp;#36;file.fileName&#10;&#10;Copyright (c) &amp;#36;today.year, Erik C. Thauvin (erik@thauvin.net)&#10;All rights reserved.&#10;&#10;Redistribution and use in source and binary forms, with or without&#10;modification, are permitted provided that the following conditions are met:&#10;&#10; Redistributions of source code must retain the above copyright notice, this&#10; list of conditions and the following disclaimer.&#10;&#10; Redistributions in binary form must reproduce the above copyright notice,&#10; this list of conditions and the following disclaimer in the documentation&#10; and/or other materials provided with the distribution.&#10;&#10; Neither the name of this project nor the names of its contributors may be&#10; used to endorse or promote products derived from this software without&#10; specific prior written permission.&#10;&#10;THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS &quot;AS IS&quot;&#10;AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE&#10;IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE&#10;DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE&#10;FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL&#10;DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR&#10;SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER&#10;CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,&#10;OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE&#10;OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." />
<option name="myName" value="Erik's Copyright Notice" />
</copyright>
</component>

3
.idea/copyright/profiles_settings.xml generated Normal file
View file

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="Erik's Copyright Notice" />
</component>

6
.idea/encodings.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View file

@ -0,0 +1,53 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<option name="myLocal" value="true" />
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="created" />
</inspection_tool>
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_VARIABLES" value="true" />
<option name="REPORT_PARAMETERS" value="false" />
<option name="REPORT_CATCH_PARAMETERS" value="false" />
</inspection_tool>
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
</inspection_tool>
<inspection_tool class="UnnecessarySemicolon" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="true" />
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="false" />
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="false" />
</inspection_tool>
</profile>
</component>

13
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AnalysisProjectProfileManager">
<option name="PROJECT_PROFILE" />
<option name="USE_PROJECT_LEVEL_SETTINGS" value="false" />
<list size="0" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_12" project-jdk-name="12" project-jdk-type="JavaSDK" />
<component name="SuppressionsComponent">
<option name="suppComments" value="[]" />
</component>
</project>

271
build.gradle.kts Normal file
View file

@ -0,0 +1,271 @@
import com.jfrog.bintray.gradle.tasks.BintrayUploadTask
import org.jetbrains.dokka.gradle.LinkMapping
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.FileInputStream
import java.util.Properties
plugins {
jacoco
java
kotlin("jvm") version "1.3.50"
`maven-publish`
id("com.github.ben-manes.versions") version "0.25.0"
id("com.gradle.build-scan") version "2.4.2"
id("com.jfrog.bintray") version "1.8.4"
id("io.gitlab.arturbosch.detekt") version "1.0.1"
id("net.thauvin.erik.gradle.semver") version "1.0.4"
id("org.jetbrains.dokka") version "0.9.18"
id("org.jetbrains.kotlin.kapt").version("1.3.50")
id("org.jmailen.kotlinter") version "2.1.1"
id("org.sonarqube") version "2.7.1"
}
group = "net.thauvin.erik"
description = "Akismet for Kotlin/Java, a client library for accesssing the Automattic Kismet (Akismet) spam comments filtering service."
val gitHub = "ethauvin/$name"
val mavenUrl = "https://github.com/$gitHub"
val deployDir = "deploy"
var isRelease = "release" in gradle.startParameter.taskNames
var semverProcessor = "net.thauvin.erik:semver:1.2.0"
val publicationName = "mavenJava"
// Load local.properties
File("local.properties").apply {
if (exists()) {
FileInputStream(this).use { fis ->
Properties().apply {
load(fis)
forEach { (k, v) ->
extra[k as String] = v
}
}
}
}
}
repositories {
jcenter()
}
dependencies {
kapt(semverProcessor)
implementation(semverProcessor)
implementation("javax.servlet:javax.servlet-api:4.0.1")
implementation("org.mockito:mockito-core:3.0.0")
compile("com.squareup.okhttp3:okhttp:4.2.0")
compile("com.squareup.okhttp3:logging-interceptor:4.2.0")
compile(kotlin("stdlib"))
testImplementation("org.testng:testng:7.0.0")
}
kapt {
arguments {
arg("semver.project.dir", projectDir)
}
}
detekt {
input = files("src/main/kotlin", "src/test/kotlin")
filters = ".*/resources/.*,.*/build/.*"
baseline = project.rootDir.resolve("detekt-baseline.xml")
}
kotlinter {
ignoreFailures = false
reporters = arrayOf("html")
experimentalRules = false
disabledRules = arrayOf("import-ordering")
}
jacoco {
toolVersion = "0.8.3"
}
sonarqube {
properties {
property("sonar.projectKey", "ethauvin_$name")
property("sonar.sourceEncoding", "UTF-8")
}
}
val sourcesJar by tasks.creating(Jar::class) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}
val javadocJar by tasks.creating(Jar::class) {
dependsOn(tasks.dokka)
from(tasks.dokka)
archiveClassifier.set("javadoc")
description = "Assembles a JAR of the generated Javadoc."
group = JavaBasePlugin.DOCUMENTATION_GROUP
}
tasks {
withType<Test> {
useTestNG()
}
withType<JacocoReport> {
reports {
xml.isEnabled = true
html.isEnabled = true
}
}
withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
withType<GenerateMavenPom> {
destination = file("$projectDir/pom.xml")
}
assemble {
dependsOn(sourcesJar, javadocJar)
}
clean {
doLast {
project.delete(fileTree(deployDir))
}
}
dokka {
outputFormat = "html"
outputDirectory = "$buildDir/javadoc"
jdkVersion = 8
val mapping = LinkMapping().apply {
dir = "src/main/kotlin"
url = "https://github.com/ethauvin/$name/blob/${project.version}/src/main/kotlin"
suffix = "#L"
}
linkMappings = arrayListOf(mapping)
includeNonPublic = false
}
val copyToDeploy by registering(Copy::class) {
from(configurations.runtime) {
exclude("annotations-*.jar")
}
from(jar)
into(deployDir)
}
register("deploy") {
description = "Copies all needed files to the $deployDir directory."
group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn("build", "jar")
outputs.dir(deployDir)
inputs.files(copyToDeploy)
mustRunAfter("clean")
}
val gitIsDirty by registering(Exec::class) {
description = "Fails if git has uncommitted changes."
group = "verification"
commandLine("git", "diff", "--quiet", "--exit-code")
}
val gitTag by registering(Exec::class) {
description = "Tags the local repository with version ${project.version}"
group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn(gitIsDirty)
if (isRelease) {
commandLine("git", "tag", "-a", project.version, "-m", "Version ${project.version}")
}
}
val bintrayUpload by existing(BintrayUploadTask::class) {
dependsOn(publishToMavenLocal, gitTag)
}
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
}
register("release") {
description = "Publishes version ${project.version} to Bintray."
group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn("wrapper", bintrayUpload)
}
"sonarqube" {
dependsOn("jacocoTestReport")
}
}
fun findProperty(s: String) = project.findProperty(s) as String?
bintray {
user = findProperty("bintray.user")
key = findProperty("bintray.apikey")
publish = isRelease
setPublications(publicationName)
pkg.apply {
repo = "maven"
name = project.name
desc = description
websiteUrl = mavenUrl
issueTrackerUrl = "$mavenUrl/issues"
githubRepo = gitHub
githubReleaseNotesFile = "README.md"
vcsUrl = "$mavenUrl.git"
setLabels("kotlin", "java", "akismet", "comments", "spam", "blog", "automattic", "kismet")
publicDownloadNumbers = true
version.apply {
name = project.version as String
desc = description
vcsTag = project.version as String
gpg.apply {
sign = true
}
}
}
}
publishing {
publications {
create<MavenPublication>(publicationName) {
from(components["java"])
artifact(sourcesJar)
artifact(javadocJar)
pom.withXml {
asNode().apply {
appendNode("name", project.name)
appendNode("description", project.description)
appendNode("url", mavenUrl)
appendNode("licenses").appendNode("license").apply {
appendNode("name", "BSD 3-Clause")
appendNode("url", "https://opensource.org/licenses/BSD-3-Clause")
}
appendNode("developers").appendNode("developer").apply {
appendNode("id", "ethauvin")
appendNode("name", "Erik C. Thauvin")
appendNode("email", "erik@thauvin.net")
}
appendNode("scm").apply {
appendNode("connection", "scm:git:$mavenUrl.git")
appendNode("developerConnection", "scm:git:git@github.com:$gitHub.git")
appendNode("url", mavenUrl)
}
appendNode("issueManagement").apply {
appendNode("system", "GitHub")
appendNode("url", "$mavenUrl/issues")
}
}
}
}
}
}

9
detekt-baseline.xml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<SmellBaseline>
<Blacklist></Blacklist>
<Whitelist>
<ID>ComplexMethod:Akismet.kt$Akismet$fun checkComment(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map&lt;String, String&gt; = emptyMap() ): Boolean</ID>
<ID>LongMethod:Akismet.kt$Akismet$fun checkComment(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map&lt;String, String&gt; = emptyMap() ): Boolean</ID>
<ID>LongParameterList:Akismet.kt$Akismet$(userIp: String, userAgent: String, referrer: String = "", permalink: String = "", type: String = "", author: String = "", authorEmail: String = "", authorUrl: String = "", content: String = "", dateGmt: String = "", postModifiedGmt: String = "", blogLang: String = "", blogCharset: String = "", userRole: String = "", recheckReason: String = "", isTest: Boolean = false, other: Map&lt;String, String&gt; = emptyMap() )</ID>
</Whitelist>
</SmellBaseline>

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

188
gradlew vendored Normal file
View file

@ -0,0 +1,188 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

100
gradlew.bat vendored Normal file
View file

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

10
settings.gradle.kts Normal file
View file

@ -0,0 +1,10 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user manual at https://docs.gradle.org/5.6.2/userguide/multi_project_builds.html
*/
rootProject.name = "akismet-kotlin"

View file

@ -0,0 +1,521 @@
/*
* Akismet.kt
*
* Copyright (c) 2019, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.akismet
import net.thauvin.erik.semver.Version
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import java.io.IOException
import java.util.logging.Level
import java.util.logging.Logger
import javax.servlet.http.HttpServletRequest
/**
* Akismet Kotlin/Java Client Library
*/
@Version(properties = "version.properties", type = "kt")
open class Akismet(apiKey: String, blog: String) {
@Suppress("unused")
companion object {
/** A blog comment. */
const val COMMENT_TYPE_COMMENT = "comment"
/** A top-level forum post. */
const val COMMENT_TYPE_FORUM_POST = "forum-post"
/** A reply to a top-level forum post. */
const val COMMENT_TYPE_REPLY = "reply"
/** A blog post. */
const val COMMENT_TYPE_BLOG_POST = "blog-post"
/** A contact form or feedback form submission. */
const val COMMENT_TYPE_CONTACT_FORM = "contact-form"
/** A new user account. */
const val COMMENT_TYPE_SIGNUP = "signup"
/** A message sent between just a few users. */
const val COMMENT_TYPE_MESSAGE = "message"
/** Administrator role */
const val ADMIN_ROLE = "administrator"
}
private val apiEndPoint = "https://%s.akismet.com/1.1/%s"
private val libUserAgent = "${GeneratedVersion.PROJECT}/${GeneratedVersion.VERSION}"
private val verifyMethod = "verify-key"
private var apiKey: String
private var blog: String
private var client: OkHttpClient
var isValidKey: Boolean = false
private set
@Suppress("MemberVisibilityCanBePrivate")
var proTip: String = ""
private set
@Suppress("MemberVisibilityCanBePrivate")
var error: String = ""
private set
@Suppress("MemberVisibilityCanBePrivate")
var degugHelp: String = ""
private set
val logger: Logger by lazy { Logger.getLogger(Akismet::class.java.simpleName) }
init {
require(!apiKey.isBlank() || apiKey.length != 12) { "An Akismet API key must be specified." }
require(!blog.isBlank()) { "A Blog URL must be specified." }
this.apiKey = apiKey
this.blog = blog
if (logger.isLoggable(Level.FINE)) {
val logging = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
logger.log(Level.FINE, message)
}
})
logging.level = HttpLoggingInterceptor.Level.BODY
client = OkHttpClient.Builder().addInterceptor(logging).build()
} else {
client = OkHttpClient()
}
}
/**
* Key Verification
*
* @see <a href="https://akismet.com/development/api/#verify-key">Akismet API</a>
*/
fun verifyKey(): Boolean {
val params = HashMap<String, String>()
params["key"] = apiKey
params["blog"] = blog
isValidKey = executeMethod(verifyMethod, FormBody.Builder().build())
return isValidKey
}
/**
* Comment Check using [HttpServletRequest][request] content.
*
* @see <a href="https://akismet.com/development/api/#comment-check">Akismet API</a>
*/
@JvmOverloads
fun checkComment(
request: HttpServletRequest,
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
return checkComment(
userIp = request.remoteAddr,
userAgent = request.getHeader("User-Agent"),
referrer = request.getHeader("Referer"),
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = buildPhpVars(request, other))
}
/**
* Comment Check
*
* @see <a href="https://akismet.com/development/api/#comment-check">Akismet API</a>
*/
@JvmOverloads
fun checkComment(
userIp: String,
userAgent: String,
referrer: String = "",
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
require(!(userIp.isBlank() && userAgent.isBlank())) { "userIp and/or userAgent are required." }
return executeMethod(
"comment-check",
buildFormBody(
userIp = userIp,
userAgent = userAgent,
referrer = referrer,
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = other))
}
/**
* Submit Spam
*
* @see <a href="https://akismet.com/development/api/#submit-spam">Akismet API</a>
*/
@JvmOverloads
fun submitSpam(
request: HttpServletRequest,
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
return submitSpam(
userIp = request.remoteAddr,
userAgent = request.getHeader("User-Agent"),
referrer = request.getHeader("Referer"),
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = buildPhpVars(request, other))
}
/**
* Submit Spam (missed spam)
*
* @see <a href="https://akismet.com/development/api/#submit-spam">Akismet API</a>
*/
@JvmOverloads
fun submitSpam(
userIp: String,
userAgent: String,
referrer: String = "",
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
return executeMethod(
"submit-spam",
buildFormBody(
userIp = userIp,
userAgent = userAgent,
referrer = referrer,
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = other))
}
/**
* Submit Ham (false positives)
*
* @see <a href="https://akismet.com/development/api/#submit-ham">Akismet API</a>
*/
@JvmOverloads
fun submitHam(
request: HttpServletRequest,
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
return submitHam(
userIp = request.remoteAddr,
userAgent = request.getHeader("User-Agent"),
referrer = request.getHeader("Referer"),
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = buildPhpVars(request, other))
}
/**
* Submit Ham
*
* @see <a href="https://akismet.com/development/api/#submit-ham">Akismet API</a>
*/
@JvmOverloads
fun submitHam(
userIp: String,
userAgent: String,
referrer: String = "",
permalink: String = "",
type: String = "",
author: String = "",
authorEmail: String = "",
authorUrl: String = "",
content: String = "",
dateGmt: String = "",
postModifiedGmt: String = "",
blogLang: String = "",
blogCharset: String = "",
userRole: String = "",
isTest: Boolean = false,
recheckReason: String = "",
other: Map<String, String> = emptyMap()
): Boolean {
return executeMethod(
"submit-ham",
buildFormBody(
userIp = userIp,
userAgent = userAgent,
referrer = referrer,
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
dateGmt = dateGmt,
postModifiedGmt = postModifiedGmt,
blogLang = blogLang,
blogCharset = blogCharset,
userRole = userRole,
isTest = isTest,
recheckReason = recheckReason,
other = other))
}
private fun executeMethod(method: String, formBody: FormBody): Boolean {
val apiUrl = buildApiUrl(method).toHttpUrlOrNull()
if (apiUrl != null) {
val request = Request.Builder().url(apiUrl).post(formBody).header("User-Agent", libUserAgent).build()
try {
val result = client.newCall(request).execute()
proTip = result.header("x-akismet-pro-tip", "").toString()
error = result.header("x-akismet-error", "").toString()
degugHelp = result.header("X-akismet-debug-help", "").toString()
val body = result.body?.string()
if (body != null) {
val response = body.trim()
if (response.equals("valid", true) ||
response.equals("true", true) ||
response.startsWith("Thanks", true)) {
return true
}
}
} catch (e: IOException) {
logger.log(Level.SEVERE, "An IO error occurred while communicating with the Akismet service.", e)
}
} else {
logger.severe("Invalid API end point URL: $method. The API Key is likely invalid.")
}
return false
}
private fun buildApiUrl(method: String): String {
if (method == verifyMethod) {
return String.format(apiEndPoint, "rest", method)
}
return String.format(apiEndPoint, apiKey, method)
}
private fun buildPhpVars(request: HttpServletRequest, other: Map<String, String>): HashMap<String, String> {
val params = HashMap<String, String>()
params["REMOTE_ADDR"] = request.remoteAddr
params["REQUEST_URI"] = request.requestURI
val names = request.headerNames
while (names.hasMoreElements()) {
val name = names.nextElement()
if (!name.equals("cookie", true)) {
params["HTTP_${name.toUpperCase()}"] = request.getHeader(name)
}
}
if (other.isEmpty()) {
params.putAll(other)
}
return params
}
private fun buildFormBody(
userIp: String,
userAgent: String,
referrer: String,
permalink: String,
type: String,
author: String,
authorEmail: String,
authorUrl: String,
content: String,
dateGmt: String,
postModifiedGmt: String,
blogLang: String,
blogCharset: String,
userRole: String,
isTest: Boolean,
recheckReason: String,
other: Map<String, String>
): FormBody {
return FormBody.Builder().apply {
add("blog", blog)
add("user_ip", userIp)
add("user_agent", userAgent)
if (referrer.isNotBlank()) {
add("referrer", referrer)
}
if (permalink.isNotBlank()) {
add("permalink", permalink)
}
if (type.isNotBlank()) {
add("comment_type", type)
}
if (author.isNotBlank()) {
add("comment_author", author)
}
if (authorEmail.isNotBlank()) {
add("comment_author_email", authorEmail)
}
if (authorUrl.isNotBlank()) {
add("comment_author_url", authorUrl)
}
if (content.isNotBlank()) {
add("comment_content", content)
}
if (dateGmt.isNotBlank()) {
add("comment_date_gmt", dateGmt)
}
if (postModifiedGmt.isNotBlank()) {
add("comment_post_modified_gmt", postModifiedGmt)
}
if (blogLang.isNotBlank()) {
add("blog_lang", blogLang)
}
if (blogCharset.isNotBlank()) {
add("blog_charset", blogCharset)
}
if (userRole.isNotBlank()) {
add("user_role", userRole)
}
if (isTest) {
add("is_test", "true")
}
if (recheckReason.isNotBlank()) {
add("recheck_reason", recheckReason)
}
other.forEach { (k, v) -> add(k, v) }
}.build()
}
}

View file

@ -0,0 +1,156 @@
/*
* AkismetTest.kt
*
* Copyright (c) 2019, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.akismet
import org.mockito.Mockito
import org.testng.Assert.assertFalse
import org.testng.Assert.assertTrue
import org.testng.Assert.expectThrows
import org.testng.annotations.BeforeClass
import org.testng.annotations.Test
import java.io.File
import java.io.FileInputStream
import java.util.Collections
import java.util.Properties
import java.util.logging.ConsoleHandler
import java.util.logging.Level
import javax.servlet.http.HttpServletRequest
/**
* The <code>AkismetTest</code> class.
*
* @author <a href="https://erik.thauvin.net/" target="_blank">Erik C. Thauvin</a>
* @created 2019-09-17
* @since 1.0
*/
fun getApiKey(): String {
var apiKey = System.getenv("AKISMET_API_KEY") ?: ""
if (apiKey.isBlank()) {
val localProps = File("local.properties")
if (localProps.exists())
localProps.apply {
if (exists()) {
FileInputStream(this).use { fis ->
Properties().apply {
load(fis)
apiKey = getProperty("AKISMET_API_KEY", "")
}
}
}
}
}
return apiKey
}
class AkismetTest {
private val userIp = "127.0.0.1"
private val userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6"
private val referrer = "http://www.google.com"
private val permalink = "http://yourblogdomainname.com/blog/post=1"
private val type = "comment"
private val author = "admin"
private val authorEmail = "test@test.com"
private val authorUrl = "http://www.CheckOutMyCoolSite.com"
private val content = "It means a lot that you would take the time to review our software. Thanks again."
private val akismet = Akismet(getApiKey(), "http://erik.thauvin.net/blog/")
private val request = Mockito.mock(HttpServletRequest::class.java)
@BeforeClass
fun beforeClass() {
with(akismet.logger) {
addHandler(ConsoleHandler().apply { level = Level.FINE })
level = Level.FINE
}
Mockito.`when`(request.remoteAddr).thenReturn(userIp)
Mockito.`when`(request.requestURI).thenReturn("/blog/post=1")
Mockito.`when`(request.getHeader("User-Agent")).thenReturn(userAgent)
Mockito.`when`(request.getHeader("Referer")).thenReturn(referrer)
Mockito.`when`(request.getHeader("Cookie")).thenReturn("name=value; name2=value2; name3=value3")
Mockito.`when`(request.getHeader("Accept-Encoding")).thenReturn("gzip")
Mockito.`when`(request.headerNames)
.thenReturn(Collections.enumeration(listOf("User-Agent", "Referer", "Cookie", "Accept-Encoding")))
}
@Test
fun constructorTest() {
expectThrows(IllegalArgumentException::class.java) {
Akismet("123456789012", "http://www.foo.com/")
Akismet("", "http://www.foo.com/")
Akismet("123456789012", "")
}
}
@Test
fun verifyKeyTest() {
assertFalse(akismet.isValidKey, "isValidKey -> false")
assertTrue(akismet.verifyKey(), "verify_key")
assertTrue(akismet.isValidKey, "isValidKey -> true")
}
@Test
fun checkCommentTest() {
// assertFalse(akismet.checkComment(userIp = userIp,
// userAgent = userAgent,
// referrer = referrer,
// permalink = permalink,
// type = type,
// author = author,
// authorEmail = authorEmail,
// authorUrl = authorUrl,
// content = content,
// userRole = Akismet.ADMIN_ROLE,
// isTest = true), "check_comment -> false")
//
// assertTrue(akismet.checkComment(userIp = userIp,
// userAgent = userAgent,
// referrer = referrer,
// permalink = permalink,
// type = type,
// author = author,
// authorEmail = authorEmail,
// authorUrl = authorUrl,
// content = content,
// isTest = true), "check_comment -> true")
assertTrue(akismet.checkComment(request,
permalink = permalink,
type = type,
author = author,
authorEmail = authorEmail,
authorUrl = authorUrl,
content = content,
isTest = true), "check_comment(request) -> true")
}
}

9
version.properties Normal file
View file

@ -0,0 +1,9 @@
#Generated by the Semver Plugin for Gradle
#Tue Sep 17 13:40:32 PDT 2019
version.buildmeta=
version.major=0
version.minor=9
version.patch=0
version.prerelease=beta
version.project=Akismet Kotlin
version.semver=0.9.0