diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e2d28fd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,186 @@
+
+# Created by https://www.gitignore.io/api/java,linux,macos,gradle,kotlin,windows,intellij
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# CMake
+cmake-build-debug/
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Ruby plugin and RubyMine
+/.rakeTasks
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Kotlin ###
+# Compiled class file
+
+# Log file
+
+# BlueJ files
+
+# Mobile Tools for Java (J2ME)
+
+# Package Files #
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### Gradle ###
+.gradle
+**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+
+# End of https://www.gitignore.io/api/java,linux,macos,gradle,kotlin,windows,intellij
diff --git a/.idea/artifacts/moztw_space_bot_jar.xml b/.idea/artifacts/moztw_space_bot_jar.xml
new file mode 100644
index 0000000..6b8304d
--- /dev/null
+++ b/.idea/artifacts/moztw_space_bot_jar.xml
@@ -0,0 +1,56 @@
+
+
+ $PROJECT_DIR$/out/artifacts/moztw_space_bot_jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..a9a1fe9
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..5806fb3
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..e208459
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..6b1ca7b
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/moztw-space-bot.iml b/.idea/modules/moztw-space-bot.iml
new file mode 100644
index 0000000..09b946d
--- /dev/null
+++ b/.idea/modules/moztw-space-bot.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/moztw-space-bot_main.iml b/.idea/modules/moztw-space-bot_main.iml
new file mode 100644
index 0000000..6a98fec
--- /dev/null
+++ b/.idea/modules/moztw-space-bot_main.iml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/moztw-space-bot_test.iml b/.idea/modules/moztw-space-bot_test.iml
new file mode 100644
index 0000000..5fff2f6
--- /dev/null
+++ b/.idea/modules/moztw-space-bot_test.iml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5919ea1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# moztw-space-bot
+org.moztw.telegram-bot:moztw-space-bot
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..ec3f9e5
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,41 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+buildscript {
+ var kotlin_version: String by extra
+ kotlin_version = "1.2.30"
+
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath(kotlinModule("gradle-plugin", kotlin_version))
+ }
+}
+
+group = "org.moztw.telegram-bot"
+version = "1.0.2-SNAPSHOT"
+
+apply {
+ plugin("java")
+ plugin("kotlin")
+}
+
+val kotlin_version: String by extra
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(kotlinModule("stdlib-jdk8", kotlin_version))
+ implementation("commons-cli", "commons-cli", "1.4")
+ implementation("org.telegram", "telegrambots", "3.6")
+ testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.1.0")
+}
+
+configure {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+}
+tasks.withType {
+ kotlinOptions.jvmTarget = "1.8"
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8d28bc2
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3cb6540
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Mar 02 11:54:18 CST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## 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=""
+
+# 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, switch paths to Windows format before running java
+if $cygwin ; 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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@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=
+
+@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
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..dfa4948
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'moztw-space-bot'
+
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..3e16bb2
--- /dev/null
+++ b/src/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: org.moztw.bot.telegram.space.MainKt
+
diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt
new file mode 100644
index 0000000..02f6129
--- /dev/null
+++ b/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt
@@ -0,0 +1,114 @@
+package org.moztw.bot.telegram.space
+
+import org.telegram.telegrambots.api.methods.groupadministration.SetChatTitle
+import org.telegram.telegrambots.api.methods.send.SendMessage
+import org.telegram.telegrambots.api.objects.CallbackQuery
+import org.telegram.telegrambots.api.objects.Chat
+import org.telegram.telegrambots.api.objects.Message
+import org.telegram.telegrambots.api.objects.Update
+import org.telegram.telegrambots.bots.TelegramLongPollingBot
+import org.telegram.telegrambots.exceptions.TelegramApiException
+import java.text.SimpleDateFormat
+import java.util.*
+
+internal class Bot(val username: String, val token: String) : TelegramLongPollingBot() {
+
+ override fun onUpdateReceived(update: Update) {
+ println("* [$dateTime] Update received: $update")
+
+ if (!(update.hasMessage() && onMessageReceived(update.message) || update.hasCallbackQuery() && onCallbackQueryReceived(update.callbackQuery)))
+ println("* [$dateTime] Update not handled: $update")
+ }
+
+ private fun onCallbackQueryReceived(callbackQuery: CallbackQuery): Boolean {
+ try {
+ Reply().getCheckList(callbackQuery)?.run { execute(this) }
+ return true
+ } catch (e: TelegramApiException) {
+ e.printStackTrace()
+ }
+
+ return false
+ }
+
+ private fun onMessageReceived(message: Message): Boolean {
+ if (isAdminChat(message.chat) && message.hasText()) {
+ if (isCommandOpen(message.text)) {
+ if (tryExecute(Caption().getCaptionOpened(chatId = generalChatId))
+ && tryExecute(Reply().getGeneralMessageOpen(chatId = generalChatId, operator = message.from))
+ && tryExecute(Reply().getMessageOpen(message = message))) {
+ for (chatId in adminChats)
+ if (chatId != message.chatId)
+ if (!tryExecute(Reply().getOtherMessageOpen(message = message, chatId = chatId, operator = message.from)))
+ return false
+ return true
+ }
+ } else if (isCommandClose(message.text)) {
+ if (tryExecute(Caption().getCaptionClosed(chatId = generalChatId))
+ && tryExecute(Reply().getGeneralMessageClose(chatId = generalChatId, operator = message.from))
+ && tryExecute(Reply().getMessageClose(message = message))) {
+ for (chatId in adminChats)
+ if (chatId != message.chatId)
+ if (!tryExecute(Reply().getOtherMessageClose(message = message, chatId = chatId, operator = message.from)))
+ return false
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ private fun tryExecute(caption: SetChatTitle): Boolean {
+ try {
+ execute(caption)
+ return true
+ } catch (e: TelegramApiException) {
+ e.printStackTrace()
+ }
+
+ return false
+ }
+
+ private fun tryExecute(sendMessage: SendMessage): Boolean {
+ try {
+ execute(sendMessage)
+ return true
+ } catch (e: TelegramApiException) {
+ e.printStackTrace()
+ }
+
+ return false
+ }
+
+ private fun isCommandClose(text: String): Boolean {
+ return text.matches("^/space_close(?:\\s|$)".toRegex()) || text.matches("^/space_close@$botUsername(?:\\s|$)".toRegex())
+ }
+
+ private fun isCommandOpen(text: String): Boolean {
+ return text.matches("^/space_open(?:\\s|$)".toRegex()) || text.matches("^/space_open@$botUsername(?:\\s|$)".toRegex())
+ }
+
+ private fun isAdminChat(chat: Chat) = adminChats.contains(chat.id)
+
+ override fun getBotUsername() = username
+
+ override fun getBotToken() = token
+
+ companion object {
+ private const val generalChatId = -1001024943275L
+ private val adminChats = arrayOf(
+ -1001060092077L, // frontierChatId
+ -1001087190182L // keyholdersChatId
+ )
+
+ private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
+
+ init {
+ dateFormat.timeZone = TimeZone.getTimeZone("Asia/Taipei")
+ }
+
+ val dateTime: String
+ get() = dateFormat.format(Date())
+ }
+}
diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Caption.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Caption.kt
new file mode 100644
index 0000000..257015a
--- /dev/null
+++ b/src/main/kotlin/org/moztw/bot/telegram/space/Caption.kt
@@ -0,0 +1,9 @@
+package org.moztw.bot.telegram.space
+
+import org.telegram.telegrambots.api.methods.groupadministration.SetChatTitle
+
+internal class Caption {
+ private fun getCaption(chatId: Long) = SetChatTitle().setChatId(chatId)!!
+ fun getCaptionOpened(chatId: Long) = getCaption(chatId).setTitle("Moz://TW(工寮開放中)")!!
+ fun getCaptionClosed(chatId: Long) = getCaption(chatId).setTitle("Moz://TW")!!
+}
diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Keyboard.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Keyboard.kt
new file mode 100644
index 0000000..9a0120a
--- /dev/null
+++ b/src/main/kotlin/org/moztw/bot/telegram/space/Keyboard.kt
@@ -0,0 +1,39 @@
+package org.moztw.bot.telegram.space
+
+import org.telegram.telegrambots.api.objects.replykeyboard.InlineKeyboardMarkup
+import org.telegram.telegrambots.api.objects.replykeyboard.buttons.InlineKeyboardButton
+import java.util.*
+
+internal class Keyboard {
+ val showButton: InlineKeyboardMarkup
+ get() = getKeyboard(arrayOf(arrayOf("📋 點此顯示工寮關門檢查列表")), arrayOf(arrayOf("list:0")))
+
+ fun getCheckList(data: Int) =
+ getKeyboard(arrayOf(
+ arrayOf((if (data and 1 > 0) "✅" else "🖼") + " 白板彙整拍照", (if (data and 2 > 0) "✅" else "🎥") + " 投影機關機"),
+ arrayOf((if (data and 4 > 0) "✅" else "🔮") + " 兩台冷氣關機", (if (data and 8 > 0) "✅" else "🔌") + " 關延長線開關"),
+ arrayOf((if (data and 16 > 0) "✅" else "🔓") + " 鎖上窗戶", (if (data and 32 > 0) "✅" else "💡") + " 關閉電燈"),
+ arrayOf("❌ 點此關閉工寮關門檢查列表")
+ ), arrayOf(
+ arrayOf("list:" + Integer.toHexString(data xor 1), "list:" + Integer.toHexString(data xor 2)),
+ arrayOf("list:" + Integer.toHexString(data xor 4), "list:" + Integer.toHexString(data xor 8)),
+ arrayOf("list:" + Integer.toHexString(data xor 16), "list:" + Integer.toHexString(data xor 32)),
+ arrayOf("hide")
+ ))
+
+ private fun getKeyboard(text: Array>, data: Array>): InlineKeyboardMarkup {
+ val keyboard = InlineKeyboardMarkup()
+ val buttons = ArrayList>()
+ for (i in 0 until text.size) {
+ val buttonRow = ArrayList()
+ for (j in 0 until text[i].size) {
+ val button = InlineKeyboardButton(text[i][j])
+ button.callbackData = data[i][j]
+ buttonRow.add(button)
+ }
+ buttons.add(buttonRow)
+ }
+ keyboard.keyboard = buttons
+ return keyboard
+ }
+}
diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Main.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Main.kt
new file mode 100644
index 0000000..dfa422b
--- /dev/null
+++ b/src/main/kotlin/org/moztw/bot/telegram/space/Main.kt
@@ -0,0 +1,38 @@
+package org.moztw.bot.telegram.space
+
+import org.apache.commons.cli.*
+import org.telegram.telegrambots.ApiContextInitializer
+import org.telegram.telegrambots.TelegramBotsApi
+import org.telegram.telegrambots.exceptions.TelegramApiRequestException
+import kotlin.system.exitProcess
+
+fun main(args: Array) {
+ val options = Options()
+
+ Option("u", "username", true, "the username of the bot").run {
+ this.isRequired = true
+ options.addOption(this)
+ }
+
+ Option("t", "token", true, "the token of the bot").run {
+ this.isRequired = true
+ options.addOption(this)
+ }
+
+ try {
+ val cmd = DefaultParser().parse(options, args)
+ val botUsername = cmd.getOptionValue("username")
+ val botToken = cmd.getOptionValue("token")
+
+ ApiContextInitializer.init()
+ try {
+ TelegramBotsApi().registerBot(Bot(username = botUsername, token = botToken))
+ } catch (e: TelegramApiRequestException) {
+ e.printStackTrace()
+ }
+ } catch (e: ParseException) {
+ println(e.message)
+ HelpFormatter().printHelp("moztw-space-bot", options)
+ exitProcess(1)
+ }
+}
diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Reply.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Reply.kt
new file mode 100644
index 0000000..e9107c4
--- /dev/null
+++ b/src/main/kotlin/org/moztw/bot/telegram/space/Reply.kt
@@ -0,0 +1,53 @@
+package org.moztw.bot.telegram.space
+
+import org.telegram.telegrambots.api.methods.send.SendMessage
+import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageReplyMarkup
+import org.telegram.telegrambots.api.objects.CallbackQuery
+import org.telegram.telegrambots.api.objects.Message
+import org.telegram.telegrambots.api.objects.User
+
+internal class Reply {
+ fun getGeneralMessageOpen(chatId: Long, operator: User) =
+ SendMessage().setChatId(chatId).setText("#工寮開門 ${Bot.dateTime}(by [${operator.firstName}](tg://user?id=${operator.id}))")!!
+
+ fun getGeneralMessageClose(chatId: Long, operator: User) =
+ SendMessage().setChatId(chatId).setText("#工寮關門 ${Bot.dateTime}(by [${operator.firstName}](tg://user?id=${operator.id}))")!!
+
+ private fun getMessage(message: Message) = SendMessage().setChatId(message.chatId!!).setReplyToMessageId(message.messageId)
+
+ fun getMessageOpen(message: Message) = getMessage(message).setText(
+ "#工寮開門 已於 ${Bot.dateTime} 送出開門資訊。\n\n" +
+ "您可以先擦拭白板並書寫 Keyholder 名稱、活動名稱以及開關門預定時間。\n" +
+ "招呼訪客時,請提醒他們於白板簽到(暱稱或 Mozillians ID e.g. moz:irvin)"
+ )!!
+
+ fun getMessageClose(message: Message) = getMessage(message).setText(
+ "#工寮關門 已於 " + Bot.dateTime + " 送出關門資訊。\n\n" +
+ "請檢視以下項目是否完成,完成後可點選來標記:"
+ ).setReplyMarkup(Keyboard().showButton)!!
+
+ private fun getOtherMessage(chatId: Long) = SendMessage().setChatId(chatId)
+
+ fun getOtherMessageOpen(message: Message, chatId: Long, operator: User) =
+ getOtherMessage(chatId).setText("#工寮開門 [${operator.firstName}](tg://user?id=${operator.id}) 已從「${message.chat.title}」群組於 ${Bot.dateTime} 送出開門資訊。")!!
+
+ fun getOtherMessageClose(message: Message, chatId: Long, operator: User) =
+ getOtherMessage(chatId).setText("#工寮關門 [${operator.firstName}](tg://user?id=${operator.id}) 已從「${message.chat.title}」群組於 ${Bot.dateTime} 送出關門資訊。")!!
+
+ fun getCheckList(query: CallbackQuery): EditMessageReplyMarkup? {
+ val data = query.data.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ when (data[0]) {
+ "hide" -> return EditMessageReplyMarkup()
+ .setChatId(query.message.chatId!!)
+ .setMessageId(query.message.messageId)
+ .setReplyMarkup(Keyboard().showButton)
+ "list" -> {
+ return if (data.size < 2) null else EditMessageReplyMarkup()
+ .setChatId(query.message.chatId!!)
+ .setMessageId(query.message.messageId)
+ .setReplyMarkup(Keyboard().getCheckList(Integer.parseInt(data[1], 16)))
+ }
+ }
+ return null
+ }
+}