├── .gitignore
├── LICENSE
├── README.md
├── actor-bots-example
├── build.gradle
├── scripts
│ └── deb
│ │ ├── bin
│ │ └── actor-bots
│ │ └── preInstall.sh
└── src
│ └── main
│ └── java
│ └── im
│ └── actor
│ └── bots
│ ├── ExampleBots.kt
│ ├── MainBotFarm.kt
│ └── MainBotFarmDebug.kt
├── actor-bots
├── build.gradle
└── src
│ └── main
│ ├── java
│ └── im
│ │ └── actor
│ │ └── bots
│ │ ├── blocks
│ │ ├── Notification.kt
│ │ └── OAuth2.kt
│ │ └── framework
│ │ ├── MagicBot.kt
│ │ ├── MagicBotEntities.kt
│ │ ├── MagicBotFarm.kt
│ │ ├── i18n
│ │ ├── I18NEngine.java
│ │ └── Strings.java
│ │ ├── parser
│ │ ├── MessageCommand.java
│ │ ├── MessageText.java
│ │ ├── ParsedMessage.java
│ │ └── ParsingUtils.java
│ │ ├── persistence
│ │ ├── KotlinExtensions.kt
│ │ ├── MagicBotPersistence.kt
│ │ └── ServerKeyValue.java
│ │ ├── stateful
│ │ ├── Expect.kt
│ │ ├── ExpectCommands.kt
│ │ ├── ExpectInput.kt
│ │ ├── ExpectRaw.kt
│ │ └── MagicBotStateful.kt
│ │ └── traits
│ │ ├── APITrait.kt
│ │ ├── AdminTrait.kt
│ │ ├── AiTrait.kt
│ │ ├── BugSnagtrait.kt
│ │ ├── DispatcherTrait.kt
│ │ ├── HTTPTrait.kt
│ │ ├── I18NTrait.kt
│ │ ├── LogTrait.kt
│ │ ├── ParseTrait.kt
│ │ └── SMTPTrait.kt
│ └── resources
│ ├── BotFather.properties
│ ├── BotFather_Ru.properties
│ ├── BotFather_Zn.properties
│ └── reference.conf
├── build.gradle
├── docs
├── README.md
├── api
│ ├── API.md
│ ├── HTTP.md
│ ├── I18N.md
│ ├── admin.md
│ ├── ai.md
│ ├── key-value-local.md
│ └── key-value-server.md
├── assets
│ └── Actor_Logo.png
└── tutorials
│ ├── bot-about.md
│ ├── bot-farm.md
│ ├── bot-implement.md
│ ├── bot-messages.md
│ ├── bot-overlord.md
│ ├── bot-persistent.md
│ ├── bot-register.md
│ ├── bot-stateful.md
│ └── web-hooks.md
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | local.properties
2 | *.iml
3 | .idea
4 | actor-bots-example/build
5 | actor-bots/build
6 | .gradle
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | Platform •
10 | Bootstrap •
11 | Bots
12 |
13 | -------
14 |
15 | # Actor Bot Platform
16 |
17 | Actor Bot platfrom allows you to implement your own bots for Actor with various modules that help you easily integrate with external services, store bot's state in built-in key-value storage or even implement your Siri-like bot with [api.ai](https://api.ai/) integration. It is based on [Kotlin language](https://kotlinlang.org).
18 |
19 | Features
20 | ============
21 | | Features
22 | --------------------------|------------------------------------------------------------
23 | :rocket: | Fast start with bot development
24 | :wrench: | Rich framework for building bots
25 | :squirrel: | Built-in Natural Language Processing with [api.ai](https://api.ai)
26 | :computer: | Easy to host anywhere
27 | :octocat: | Community supported
28 |
29 | ## Getting Started
30 |
31 | All documentation and tutorials are at [docs](docs) directory.
32 |
33 | ## Example Bot
34 |
35 | ```kotlin
36 |
37 | import im.actor.bots.framework.*
38 |
39 | class EchoBot(scope: MagicForkScope) : MagicBotFork(scope) {
40 |
41 | override fun onMessage(message: MagicBotMessage) {
42 | when (message) {
43 | is MagicBotTextMessage -> {
44 | sendText("Received: ${message.text}")
45 | }
46 | }
47 | }
48 | }
49 |
50 | fun main(args: Array) {
51 | farm("BotFarm") {
52 | bot(EchoBot::class) {
53 | name = "echo"
54 | token = ""
55 | }
56 | }
57 | }
58 | ```
59 |
60 | ## Community
61 |
62 | You can reach Actor community in our [Actor Open Source group](https://actor.im/oss).
63 |
64 | ## License
65 |
66 | Licensed under [Apache 2.0](LICENSE)
67 |
--------------------------------------------------------------------------------
/actor-bots-example/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.0.1-2'
3 | repositories {
4 | jcenter()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
9 | classpath 'com.netflix.nebula:gradle-ospackage-plugin:3.1.0'
10 | }
11 | }
12 |
13 | repositories {
14 | jcenter()
15 | mavenCentral()
16 | }
17 |
18 | apply plugin: 'java'
19 | apply plugin: 'kotlin'
20 | apply plugin: 'application'
21 | apply plugin: 'nebula.deb'
22 |
23 | group 'im.actor'
24 | version '1.0-SNAPSHOT'
25 |
26 | sourceCompatibility = 1.8
27 |
28 | mainClassName = "im.actor.bots.MainBotFarmKt"
29 |
30 | dependencies {
31 |
32 | compile project(':actor-bots')
33 |
34 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
35 |
36 | testCompile group: 'junit', name: 'junit', version: '4.11'
37 | }
38 |
39 | sourceSets {
40 | main.java.srcDirs += 'src/main/kotlin'
41 | }
42 |
43 | task actorBotsDeb(type: Deb) {
44 | release '1'
45 |
46 | into "/usr/lib/actor-bots"
47 |
48 | user 'actor'
49 |
50 | dependsOn installApp
51 |
52 | preInstall file('scripts/deb/preInstall.sh')
53 |
54 | from(jar.outputs.files) {
55 | into '/usr/lib/actor-bots'
56 | }
57 | from(configurations.runtime) {
58 | into '/usr/lib/actor-bots'
59 | }
60 | from('scripts/deb/bin') {
61 | into '/usr/bin'
62 | fileMode 0555
63 | }
64 | from('src/main/resources') {
65 | into '/usr/lib/actor-bots'
66 | }
67 | from('home') {
68 | createDirectoryEntry = true
69 | fileMode 0500
70 | into 'home'
71 | }
72 | }
--------------------------------------------------------------------------------
/actor-bots-example/scripts/deb/bin/actor-bots:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | DEFAULT_JVM_OPTS="-Xmx64m"
4 |
5 | die ( ) {
6 | echo
7 | echo "$*"
8 | echo
9 | exit 1
10 | }
11 |
12 | # Determine the Java command to use to start the JVM.
13 | if [ -n "$JAVA_HOME" ] ; then
14 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
15 | # IBM's JDK on AIX uses strange locations for the executables
16 | JAVACMD="$JAVA_HOME/jre/sh/java"
17 | else
18 | JAVACMD="$JAVA_HOME/bin/java"
19 | fi
20 | if [ ! -x "$JAVACMD" ] ; then
21 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
22 | Please set the JAVA_HOME variable in your environment to match the
23 | location of your Java installation."
24 | fi
25 | else
26 | JAVACMD="java"
27 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | Please set the JAVA_HOME variable in your environment to match the
29 | location of your Java installation."
30 | fi
31 |
32 | ARGS="$@"
33 | NEW_ARGS[0]=''
34 | IDX=0
35 | for ARG in "$@"; do
36 | case $ARG in
37 | '-D'*)
38 | JVM_OPTS="$JVM_OPTS $ARG"
39 | ;;
40 | *)
41 | NEW_ARGS[$IDX]="$ARG"
42 | let IDX=$IDX+1
43 | ;;
44 | esac
45 | done
46 | ARGS="${NEW_ARGS[@]}"
47 |
48 | function splitJvmOpts() {
49 | JVM_OPTS=("$@")
50 | }
51 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JVM_OPTS
52 |
53 | exec "$JAVACMD" "${JVM_OPTS[@]}" -cp "/usr/lib/actor-bots/*" im.actor.bots.MainBotFarmKt $ARGS
--------------------------------------------------------------------------------
/actor-bots-example/scripts/deb/preInstall.sh:
--------------------------------------------------------------------------------
1 | export USER=actor
2 | export GROUP=actor
3 |
4 | # check that owner group exists
5 | if [ -z "$(getent group ${GROUP})" ]; then
6 | groupadd ${GROUP}
7 | fi
8 |
9 | # check that user exists
10 | if [ -z "$(getent passwd ${USER})" ]; then
11 | useradd --gid ${GROUP} ${USER}
12 | fi
13 |
14 | # (optional) check that user belongs to group
15 | if ! id -G -n ${USER} | grep -qF ${GROUP} ; then
16 | usermod -a -G ${GROUP} ${USER}
17 | fi
18 |
--------------------------------------------------------------------------------
/actor-bots-example/src/main/java/im/actor/bots/ExampleBots.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots
2 |
3 | import im.actor.bots.framework.MagicBotFork
4 | import im.actor.bots.framework.MagicBotMessage
5 | import im.actor.bots.framework.MagicBotTextMessage
6 | import im.actor.bots.framework.MagicForkScope
7 | import im.actor.bots.framework.persistence.MagicPersistentBot
8 | import im.actor.bots.framework.stateful.MagicStatefulBot
9 | import im.actor.bots.framework.stateful.isText
10 | import im.actor.bots.framework.stateful.oneShot
11 | import im.actor.bots.framework.stateful.text
12 | import org.json.JSONObject
13 |
14 | /**
15 | * Very simple echo bot that forwards message
16 | */
17 | class EchoBot(scope: MagicForkScope) : MagicBotFork(scope) {
18 |
19 | override fun onMessage(message: MagicBotMessage) {
20 | when (message) {
21 | is MagicBotTextMessage -> {
22 | sendText("Received: ${message.text}")
23 | }
24 | }
25 | }
26 | }
27 |
28 | class EchoStatefulBot(scope: MagicForkScope) : MagicStatefulBot(scope) {
29 | override fun configure() {
30 | // Configure group behaviour
31 | ownNickname = "echo"
32 | enableInGroups = true
33 | onlyWithMentions = false
34 |
35 | oneShot("/start") {
36 | sendText("Hi, i'm simple echo bot, send me text and i'll send it back.")
37 | }
38 |
39 | oneShot("default") {
40 | if (isText) {
41 | sendText(text)
42 | }
43 | }
44 |
45 | }
46 |
47 | }
48 |
49 | /**
50 | * Echo persistent bot that keeps it's state between restart
51 | */
52 | class EchoPersistentBot(scope: MagicForkScope) : MagicPersistentBot(scope) {
53 |
54 | var receivedCount: Int = 0
55 |
56 | override fun onRestoreState(state: JSONObject) {
57 | receivedCount = state.optInt("counter", 0)
58 | }
59 |
60 | override fun onMessage(message: MagicBotMessage) {
61 | sendText("Received ${receivedCount++} messages")
62 | }
63 |
64 | override fun onSaveState(state: JSONObject) {
65 | state.put("counter", receivedCount)
66 | }
67 | }
--------------------------------------------------------------------------------
/actor-bots-example/src/main/java/im/actor/bots/MainBotFarm.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots
2 |
3 | import im.actor.bots.framework.farm
4 | import im.actor.bots.framework.traits.sharedBugSnagClient
5 |
6 | fun main(args: Array) {
7 |
8 |
9 | farm("NewFarm") {
10 |
11 |
12 | bot(EchoStatefulBot::class) {
13 | name = "BOT_NAME_HERE"
14 | token = "YOUR_TOKEN_HERE"
15 | }
16 |
17 | }
18 | }
--------------------------------------------------------------------------------
/actor-bots-example/src/main/java/im/actor/bots/MainBotFarmDebug.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots
2 |
3 | import im.actor.bots.framework.farm
4 |
5 | fun main(args: Array) {
6 | farm("bots") {
7 |
8 | // Stewie
9 | // bot(WunderListDebugBot::class) {
10 | // name = "sample"
11 | // token = "b741a02405054df6104d83452db0170b61267c19"
12 | // overlordClazz = WunderListOverlord::class.java
13 | // }
14 |
15 | // Rabbit MQ
16 | // bot(RabbitMQBot::class) {
17 | // name = "rabbitmq"
18 | // token = "8a5e8010ce1131abe7d5928e382fec2b0db0d78b"
19 | // }
20 |
21 | // bot(StickerBot::class) {
22 | // name = "sample"
23 | // token = "b741a02405054df6104d83452db0170b61267c19"
24 | // }
25 |
26 | // bots(JarvisBot::class) {
27 | // name = "jarvis"
28 | // token = "00720484d06cc4b77d1eb17033bd36226f5ec339"
29 | // }
30 | }
31 | }
--------------------------------------------------------------------------------
/actor-bots/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | jcenter()
4 | mavenCentral()
5 | }
6 | dependencies {
7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.1-2"
8 | }
9 | }
10 |
11 | repositories {
12 | jcenter()
13 | mavenCentral()
14 | }
15 |
16 | apply plugin: 'java'
17 | apply plugin: 'kotlin'
18 |
19 | group 'im.actor'
20 | version '1.0-SNAPSHOT'
21 |
22 | sourceCompatibility = 1.8
23 |
24 | sourceSets {
25 | main.java.srcDirs += 'src/main/kotlin'
26 | }
27 |
28 | dependencies {
29 | compile 'im.actor:actor-botkit:1.0.67'
30 | compile 'im.actor:shardakka_2.11:0.1.16'
31 | compile 'org.iq80.leveldb:leveldb:0.7'
32 | compile 'org.fusesource.leveldbjni:leveldbjni-all:1.8'
33 | compile 'org.apache.httpcomponents:httpclient:4.5.1'
34 | compile 'org.json:json:20150729'
35 | compile 'org.jdom:jdom2:2.0.6'
36 | compile 'com.bugsnag:bugsnag:1.2.8'
37 | compile 'org.codemonkey.simplejavamail:simple-java-mail:2.4+'
38 |
39 | compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.1-2"
40 | runtime "org.jetbrains.kotlin:kotlin-reflect:1.0.1-2"
41 | compile 'com.fasterxml.jackson.module:jackson-module-kotlin:2.6.5-2'
42 |
43 | compile 'commons-io:commons-io:2.4'
44 | compile 'commons-validator:commons-validator:1.4.1'
45 |
46 | testCompile group: 'junit', name: 'junit', version: '4.11'
47 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/blocks/Notification.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.blocks
2 |
3 | import im.actor.bots.framework.*
4 | import im.actor.bots.framework.stateful.*
5 | import org.json.JSONArray
6 | import org.json.JSONObject
7 | import java.util.*
8 |
9 | open class NotificationBot(scope: MagicForkScope) : MagicStatefulBot(scope) {
10 |
11 | var welcomeMessage = "Hello! I am notification bots and i can send you various notifications. " +
12 | "Just send me /subscribe and i will start to broadcast messages to you"
13 |
14 | override fun configure() {
15 |
16 | oneShot("/start") {
17 | if (isAdmin(scope.sender)) {
18 | sendText("Hello! I can help you distribute notifications to people. " +
19 | "They just need to send me message '/subscribe' to subscribe to notifications " +
20 | "and i will broadcast it to them.\n" +
21 | "I have following commands:\n" +
22 | "- */hook_url* - Get web hook url\n" +
23 | "- */send* - Broadcast message\n" +
24 | "- */subscribe_admin* - Subscribing to admin events\n" +
25 | "- */unsubscribe_admin* - unsubscribing to admin events\n")
26 | } else {
27 | sendText(welcomeMessage)
28 | }
29 | }
30 |
31 | oneShot("/subscribe") {
32 | sendText("Congratulations! You have successfully *subscribed* to my notifications! To unsubscribe send /unsubscribe")
33 | sendToOverlord(NotificationOverlord.Subscribe(scope.peer))
34 | }
35 |
36 | oneShot("/unsubscribe") {
37 | sendText("You have *unsubscribed* from my notifications. Feel free to subscribe again with /subscribe command")
38 | sendToOverlord(NotificationOverlord.Unsubscribe(scope.peer))
39 | }
40 |
41 | oneShot("/subscribe_admin") {
42 | sendText("Congratulations! You have successfully *subscribed* to admin notifications! To unsubscribe send /unsubscribe_admin")
43 | sendToOverlord(NotificationOverlord.SubscribeAdmin(scope.peer))
44 | }
45 |
46 | oneShot("/unsubscribe_admin") {
47 | sendText("You have *unsubscribed* from admin notifications. Feel free to subscribe again with /subscribe_admin command")
48 | sendToOverlord(NotificationOverlord.UnsubscribeAdmin(scope.peer))
49 | }
50 |
51 | oneShot("/hook_url") {
52 | if (isSenderAdmin()) {
53 | var hook = scope.botKeyValue.getStringValue("notification_url")
54 | if (hook == null) {
55 | hook = createHook("notification_hook_url")
56 | scope.botKeyValue.setStringValue("notification_url", hook)
57 | }
58 | sendText("Notification hook is: *$hook*")
59 | } else {
60 | sendText("You is not allowed to do this")
61 | }
62 | }
63 |
64 | raw("/send") {
65 |
66 | var broadcastMessage: String? = null
67 |
68 | before {
69 | sendText("What do you want to broadcast?")
70 | goto("message")
71 | }
72 |
73 | expectInput("message") {
74 | received {
75 | broadcastMessage = text
76 | sendText("Success. Are you sure want to send message $broadcastMessage? Send yes or no in response.")
77 | goto("confirm")
78 | }
79 | validate {
80 | if (!isText) {
81 | sendText("Please, send valid text message")
82 | return@validate false
83 | }
84 | return@validate true
85 | }
86 | }
87 |
88 | expectInput("confirm") {
89 | received {
90 | when (text.toLowerCase()) {
91 | "yes" -> {
92 | sendToOverlord(NotificationOverlord.DoBroadcast(broadcastMessage!!))
93 | sendText("Message sent!")
94 | goto("main")
95 | }
96 | "no" -> {
97 | sendText("Message send cancelled.")
98 | goto("main")
99 | }
100 |
101 | }
102 | }
103 | validate {
104 | if (isText) {
105 | when (text.toLowerCase()) {
106 | "yes" -> {
107 | return@validate true
108 | }
109 | "no" -> {
110 | return@validate true
111 | }
112 | }
113 | }
114 | sendText("Please, send yes or no.")
115 | return@validate false
116 | }
117 | }
118 | }
119 | }
120 |
121 | fun isSenderAdmin(): Boolean {
122 | if (scope.sender == null) {
123 | return false
124 | }
125 | val sender = getUser(scope.sender!!.id())
126 | if (sender.username.isPresent && admins.contains(sender.username.get())) {
127 | return true
128 | }
129 | return false
130 | }
131 | }
132 |
133 | class NotificationOverlord(scope: MagicOverlordScope) : MagicOverlord(scope) {
134 |
135 | val keyValue = scope.botKeyValue
136 | val subscribers = ArrayList()
137 | val adminSubscribers = ArrayList()
138 |
139 | // Events
140 |
141 | fun onText(text: String) {
142 | for (s in subscribers) {
143 | sendText(s, text)
144 | }
145 |
146 | onAdminText("Broadcasted message\n$text")
147 | }
148 |
149 | fun onAdminText(text: String) {
150 | for (s in adminSubscribers) {
151 | sendText(s, text)
152 | }
153 | }
154 |
155 | fun onSubscribe(peer: OutPeer) {
156 | if (subscribers.contains(peer)) {
157 | return
158 | }
159 | subscribers.add(peer)
160 | saveSubscribers()
161 |
162 | onAdminText("Subscribed $peer")
163 | }
164 |
165 | fun onUnsubscribe(peer: OutPeer) {
166 | subscribers.remove(peer)
167 | saveSubscribers()
168 |
169 | onAdminText("Unsubscribed $peer")
170 | }
171 |
172 | fun onSubscribeAdmin(peer: OutPeer) {
173 | if (adminSubscribers.contains(peer)) {
174 | return
175 | }
176 | adminSubscribers.add(peer)
177 | saveSubscribers()
178 | }
179 |
180 | fun onUnsubscribeAdmin(peer: OutPeer) {
181 | adminSubscribers.remove(peer)
182 | saveSubscribers()
183 | }
184 |
185 | fun saveSubscribers() {
186 | val storage = JSONObject()
187 |
188 | val peers = JSONArray()
189 | for (s in subscribers) {
190 | peers.put(s.toJson())
191 | }
192 | storage.put("peers", peers)
193 |
194 | val adminPeers = JSONArray()
195 | for (s in adminSubscribers) {
196 | adminPeers.put(s.toJson())
197 | }
198 | storage.put("adminPeers", adminPeers)
199 |
200 | keyValue.setStringValue("storage", storage.toString())
201 | }
202 |
203 | fun loadSubscribers() {
204 | subscribers.clear()
205 | adminSubscribers.clear()
206 | try {
207 | val storage = JSONObject(keyValue.getStringValue("storage"))
208 | val peers = storage.getJSONArray("peers")
209 | for (i in 0..peers.length()) {
210 | try {
211 | subscribers.add(outPeerFromJson(peers.getJSONObject(i)))
212 | } catch(e: Exception) {
213 | e.printStackTrace()
214 | }
215 | }
216 | val adminPeers = storage.getJSONArray("adminPeers")
217 | for (i in 0..adminPeers.length()) {
218 | try {
219 | adminSubscribers.add(outPeerFromJson(peers.getJSONObject(i)))
220 | } catch(e: Exception) {
221 | e.printStackTrace()
222 | }
223 | }
224 | } catch(e: Exception) {
225 | e.printStackTrace()
226 | }
227 | }
228 |
229 | // Processor
230 |
231 | override fun preStart() {
232 | super.preStart()
233 |
234 | loadSubscribers()
235 | }
236 |
237 | override fun onReceive(update: Any?) {
238 | if (update is Subscribe) {
239 | onSubscribe(update.peer)
240 | } else if (update is Unsubscribe) {
241 | onUnsubscribe(update.peer)
242 | } else if (update is SubscribeAdmin) {
243 | onSubscribeAdmin(update.peer)
244 | } else if (update is UnsubscribeAdmin) {
245 | onUnsubscribeAdmin(update.peer)
246 | } else if (update is DoBroadcast) {
247 | onText(update.message)
248 | } else {
249 | super.onReceive(update)
250 | }
251 | }
252 |
253 | override fun onWebHookReceived(hook: HookData) {
254 | if (hook.jsonBody != null) {
255 | val text = hook.jsonBody.optString("text")
256 | if (text != null) {
257 | onText(text)
258 | }
259 | }
260 | }
261 |
262 | data class DoBroadcast(val message: String)
263 |
264 | data class Subscribe(val peer: OutPeer)
265 |
266 | data class Unsubscribe(val peer: OutPeer)
267 |
268 | data class SubscribeAdmin(val peer: OutPeer)
269 |
270 | data class UnsubscribeAdmin(val peer: OutPeer)
271 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/blocks/OAuth2.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.blocks
2 |
3 | import im.actor.bots.framework.*
4 | import im.actor.bots.framework.persistence.ServerKeyValue
5 | import im.actor.bots.framework.stateful.MagicStatefulBot
6 | import im.actor.bots.framework.stateful.oneShot
7 | import im.actor.bots.framework.traits.HTTPTrait
8 | import im.actor.bots.framework.traits.HTTPTraitImpl
9 | import org.json.JSONObject
10 | import java.util.*
11 |
12 | private val OAuth2WebHookName = "oauth_callback_url"
13 | private var apiCache = HashMap>()
14 |
15 | abstract class OAuth2Bot(scope: MagicForkScope) : MagicStatefulBot(scope) {
16 |
17 | // Used for generating random state string
18 | private val random = Random()
19 |
20 | override fun configure() {
21 |
22 | // Configuring built-in admin method
23 | if (isAdminScope()) {
24 | oneShot("/token") {
25 | sendText("OAuth2 Callback url is: " + getOAuthCallback())
26 | }
27 | }
28 |
29 | if (scope.peer.isPrivate) {
30 | var api = apiForUid(scope.peer.id)
31 | oneShot("/login") {
32 | if (api.isAuthenticated()) {
33 | sendText("You is already authenticated. Send [/logout](send:/logout) to logout.")
34 | } else {
35 | sendText("Please, open url: ${api.authenticateUrl(registerOAuth2Request())}")
36 | }
37 | }
38 | oneShot("/logout") {
39 | if (!api.isAuthenticated()) {
40 | sendText("You is not authenticated. Please, send [/login](send:/login) to log in.")
41 | } else {
42 | api.revokeAuthentication()
43 | api.saveAuthState()
44 | sendText("You successfully logged out.")
45 | }
46 | }
47 | } else {
48 | oneShot("/login") {
49 | sendText("Please, do it in private")
50 | }
51 | oneShot("/logout") {
52 | sendText("Please, do it in private")
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * Getting API for UID with built-in cache
59 | */
60 | fun apiForUid(uid: Int): T {
61 | synchronized(apiCache) {
62 | var res = apiCache[scope.name]
63 | if (res != null) {
64 | val cached = res[uid]
65 | if (cached != null) {
66 | return cached as T
67 | }
68 | val resApi = createApi(uid)
69 | res.put(uid, resApi)
70 | return resApi
71 | } else {
72 | res = HashMap()
73 | val resApi = createApi(uid)
74 | res.put(uid, resApi)
75 | apiCache.put(scope.name, res)
76 | return resApi
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * Create API for user with UID
83 | */
84 | abstract fun createApi(uid: Int): T
85 |
86 |
87 | /**
88 | * Called when authentication successful
89 | */
90 | open fun onAuthSuccess() {
91 |
92 | }
93 |
94 | /**
95 | * Called when authentication failed
96 | */
97 | open fun onAuthFailure() {
98 |
99 | }
100 |
101 | //
102 | // Implementation
103 | //
104 |
105 | /**
106 | * Generating random state string
107 | */
108 | fun randomState(): String {
109 | return Math.abs(random.nextLong()).toString()
110 | }
111 |
112 | /**
113 | * Implement this method for handling OAuth2 Response
114 | */
115 | fun onOAuthResponse(code: String) {
116 | val api = apiForUid(scope.peer.id)
117 | if (api.authenticate(code)) {
118 | api.saveAuthState()
119 | onAuthSuccess()
120 | } else {
121 | onAuthFailure()
122 | }
123 | }
124 |
125 | /**
126 | * Registering OAuth2 request with internally generated state
127 | */
128 | fun registerOAuth2Request(): String {
129 | val state = randomState()
130 | registerOAuth2Request(state)
131 | return state
132 | }
133 |
134 | /**
135 | * Registering OAuth2 request
136 | */
137 | fun registerOAuth2Request(state: String) {
138 | if (!scope.peer.isPrivate) {
139 | throw RuntimeException("Can't register state not from private chat")
140 | }
141 | sendToOverlord(RegisterOAuth2State(state, scope.peer))
142 | }
143 |
144 | /**
145 | * Get This OAuth2 Callback to entering to OAuth2 provider
146 | */
147 | fun getOAuthCallback(): String {
148 | val cached = scope.botKeyValue.getStringValue("oauth_callback_url")
149 | if (cached != null) {
150 | return cached
151 | }
152 | val res = createHook(OAuth2WebHookName) ?: throw RuntimeException("Unable to register hook")
153 | scope.botKeyValue.setStringValue("oauth_callback_url", res)
154 | return res
155 | }
156 |
157 | override fun onOverlordMessage(message: Any) {
158 | when (message) {
159 | is OAuth2Result -> {
160 | onOAuthResponse(message.code)
161 | }
162 | }
163 | }
164 | }
165 |
166 | /**
167 | * Subclass from this overlord for providing OAuth2 support
168 | */
169 | open class OAuth2Overlord(scope: MagicOverlordScope) : MagicOverlord(scope) {
170 |
171 | private val ids = HashMap()
172 |
173 | override fun onWebHookReceived(hook: HookData) {
174 | when (hook.name) {
175 | OAuth2WebHookName -> {
176 | var args = hook.query.split("&")
177 | var code: String? = null
178 | var state: String? = null
179 | for (a in args) {
180 | val parts = a.split("=")
181 | if (parts.size != 2) {
182 | continue
183 | }
184 | when (parts[0]) {
185 | "code" -> {
186 | code = parts[1]
187 | }
188 | "state" -> {
189 | state = parts[1]
190 | }
191 | }
192 | }
193 |
194 | if (code != null && state != null) {
195 | val peer = ids[state]
196 | if (peer != null) {
197 | sendToForks(peer, OAuth2Result(code))
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
204 | override fun onReceive(update: Any?) {
205 | when (update) {
206 | is RegisterOAuth2State -> {
207 | ids.put(update.state, update.peer)
208 | }
209 | else -> {
210 | super.onReceive(update)
211 | }
212 | }
213 | }
214 | }
215 |
216 | data class OAuth2Result(val code: String)
217 | data class RegisterOAuth2State(val state: String, val peer: OutPeer)
218 |
219 | abstract class OAuth2Api(val clientId: String, val clientSecret: String, val storage: ServerKeyValue, val uid: Int) :
220 | HTTPTrait by HTTPTraitImpl() {
221 |
222 | init {
223 | val auth = storage.getJSONValue("auth_$uid")
224 | if (auth != null) {
225 | loadAuthState(auth)
226 | }
227 | }
228 |
229 | abstract fun isAuthenticated(): Boolean
230 |
231 | abstract fun authenticate(authCode: String): Boolean
232 |
233 | abstract fun authenticateUrl(state: String): String
234 |
235 | abstract fun revokeAuthentication()
236 |
237 | fun saveAuthState() {
238 | var obj = JSONObject()
239 | saveAuthState(obj)
240 | storage.setJSONValue("auth_$uid", obj)
241 | }
242 |
243 | protected abstract fun saveAuthState(obj: JSONObject)
244 |
245 | protected abstract fun loadAuthState(obj: JSONObject)
246 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/MagicBot.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework
2 |
3 | import akka.actor.ActorRef
4 | import akka.actor.Props
5 | import akka.actor.UntypedActor
6 | import im.actor.botkit.RemoteBot
7 | import im.actor.bots.BotMessages
8 | import im.actor.bots.framework.parser.MessageCommand
9 | import im.actor.bots.framework.parser.ParsedMessage
10 | import im.actor.bots.framework.persistence.ServerKeyValue
11 | import im.actor.bots.framework.traits.*
12 | import org.json.JSONObject
13 | import scala.Option
14 | import scala.concurrent.Future
15 | import java.nio.charset.Charset
16 | import java.util.*
17 |
18 | /**
19 | * Configuration of Bot
20 | * @param name Unique name of Bot
21 | * @param clazz Class of bots
22 | * @param overlordClazz Optional class of bots's overlord
23 | * @param token Bot's token in Actor Platform
24 | * @param endpoint Bot's API Endpoint
25 | * @param traceHook Optional WebHook for logging
26 | */
27 | class MagicBotConfig(val name: String,
28 | val clazz: Class<*>,
29 | val overlordClazz: Class<*>?,
30 | val token: String,
31 | val endpoint: String,
32 | val traceHook: String?) {
33 |
34 | }
35 |
36 | /**
37 | * Bot's Scope: Context information for bots
38 | * @param name Unique name of bots
39 | * @param peer Fork's peer
40 | * @param sender Sender of message
41 | * @param bot Magical Remote Bot, required to make requests
42 | * @param forkKeyValue Fork's key-value. Useful for storing conversation state.
43 | * @param botKeyValue All bots's key value. Useful for storing bots's state.
44 | * @param overlord Optional Overlord's actor ref
45 | */
46 | class MagicForkScope(val name: String,
47 | var peer: OutPeer,
48 | var sender: BotMessages.User?,
49 | val bot: MagicalRemoteBot,
50 | val forkKeyValue: ServerKeyValue,
51 | val botKeyValue: ServerKeyValue,
52 | val overlord: ActorRef?) {
53 | }
54 |
55 | /**
56 | * Overlord's scope
57 | * @param botKeyValue Bot's key value
58 | * @param bot Magical Remote Bot, required to make requests
59 | */
60 | class MagicOverlordScope(val botKeyValue: ServerKeyValue,
61 | val rootRef: ActorRef,
62 | val bot: MagicalRemoteBot)
63 |
64 | /**
65 | * Magical Remote Bot
66 | * @param config Configuration for bots
67 | */
68 | open class MagicalRemoteBot(val config: MagicBotConfig) :
69 | HTTPTrait by HTTPTraitImpl(),
70 | BugSnag by BugSnagImpl(),
71 | RemoteBot(config.token, config.endpoint) {
72 |
73 | private var child: ActorRef? = null
74 |
75 | override fun preStart() {
76 | super.preStart()
77 | child = context().actorOf(Props.create(MagicChildBot::class.java, this, config), "child")
78 | }
79 |
80 | /**
81 | * Handling New Message received
82 | */
83 | override fun onMessage(p0: BotMessages.Message?) {
84 |
85 | child?.tell(p0, self())
86 |
87 | // Tracing
88 | var message = "<<<<<< ${p0!!.peer().toUsable().toUniqueId()}"
89 | val sender = getUser(p0.sender().id())
90 | if (sender.emailContactRecords.size > 0) {
91 | message += "\nby *${sender.name()}* (${sender.emailContactRecords.first().email()})"
92 | } else if (sender.phoneContactRecords.size > 0) {
93 | message += "\nby *${sender.name()}* (+${sender.phoneContactRecords.first().phone()})"
94 | } else {
95 | return
96 | }
97 | message += "\n```\n${p0.message()}```"
98 | trace(message)
99 | }
100 |
101 | /**
102 | * Handling Raw Update received
103 | */
104 | override fun onRawUpdate(p0: BotMessages.RawUpdate?) {
105 | child?.tell(p0, self())
106 |
107 | // Tracing
108 | trace("<<<<<< Update\n```\n$p0\n```")
109 | }
110 |
111 | override fun requestSendMessage(peer: BotMessages.OutPeer?, randomId: Long, message: BotMessages.MessageBody?): Future? {
112 | val res = super.requestSendMessage(peer, randomId, message)
113 | // Tracing
114 | trace(">>>>>> ($peer)\n```\n$message\n```")
115 | return res
116 | }
117 |
118 | private fun trace(msg: String) {
119 | if (config.traceHook != null) {
120 | urlPostJson(config.traceHook, Json.JsonObject(JSONObject().apply {
121 | put("text", msg)
122 | }))
123 | }
124 | }
125 |
126 | override fun preRestart(reason: Throwable?, message: Option?) {
127 | super.preRestart(reason, message)
128 |
129 | logException(reason)
130 | }
131 |
132 | /**
133 | * Child Bot to avoid dead locks in RPC requests
134 | */
135 | class MagicChildBot(val bot: MagicalRemoteBot, val config: MagicBotConfig) :
136 | BugSnag by BugSnagImpl(),
137 | UntypedActor() {
138 |
139 | /**
140 | * Bot's key value storage
141 | */
142 | private var botKeyValue = ServerKeyValue(bot)
143 |
144 | /**
145 | * Overlord for bots
146 | */
147 | private var overlord: ActorRef? = null
148 |
149 | override fun preStart() {
150 | super.preStart()
151 |
152 | if (config.overlordClazz != null) {
153 | overlord = context().actorOf(Props.create(config.overlordClazz, MagicOverlordScope(botKeyValue, self(), bot)), "overlord")
154 | }
155 | }
156 |
157 | /**
158 | * Message received handler: Forwarding to specific fork
159 | */
160 | final override fun onReceive(message: Any?) {
161 | when (message) {
162 | is BotMessages.Message -> {
163 | peerActor(message.peer().toUsable(), message.sender()).tell(message, self())
164 | }
165 | is BotMessages.RawUpdate -> {
166 | overlord?.tell(message, self())
167 | }
168 | is OverlordMessage -> {
169 | peerActor(message.peer, null).tell(message, self())
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * Building peer's Actor
176 | */
177 | fun peerActor(peer: OutPeer, sender: BotMessages.UserOutPeer?): ActorRef {
178 | val peerId = peer.toUniqueId()
179 | val cached = context().child(peerId)
180 | if (cached.nonEmpty()) {
181 | return cached.get()
182 | } else {
183 | val scope = MagicForkScope(bot.config.name, peer, if (sender != null) bot.getUser(sender.id()) else null, bot,
184 | ServerKeyValue(bot, "peer_$peerId"), botKeyValue, overlord)
185 | return context().actorOf(Props.create(bot.config.clazz, scope), peerId)
186 | }
187 | }
188 |
189 | override fun preRestart(reason: Throwable?, message: Option?) {
190 | super.preRestart(reason, message)
191 |
192 | logException(reason)
193 | }
194 | }
195 | }
196 |
197 | private data class OverlordMessage(val peer: OutPeer, val message: Any)
198 |
199 | /**
200 | * Main Magic Bot
201 | */
202 | abstract class MagicBotFork(val scope: MagicForkScope) :
203 | HTTPTrait by HTTPTraitImpl(),
204 | AiTrait by AiTraitImpl(),
205 | I18NTrait by I18NTraitImpl(),
206 | LogTrait by LogTraitImpl(),
207 | APITraitScoped by APITraitScopedImpl(scope.peer, scope.bot),
208 | AdminTraitScoped by AdminTraitScopedImpl(scope),
209 | DispatchTrait by DispatchTraitImpl(),
210 | BugSnag by BugSnagImpl(),
211 | UntypedActor() {
212 |
213 | /**
214 | * Enable bots handling in groups.
215 | */
216 | var enableInGroups = true
217 |
218 | /**
219 | * Enable message handling only with mentions.
220 | */
221 | var onlyWithMentions = true
222 |
223 | /**
224 | * Bot's nickname. Used for filtering messages.
225 | */
226 | var ownNickname: String? = null
227 |
228 | /**
229 | * Constructing Bot
230 | */
231 | init {
232 | initLog(this)
233 | initDispatch(this)
234 | }
235 |
236 | /**
237 | * Handling messages
238 | */
239 | abstract fun onMessage(message: MagicBotMessage)
240 |
241 | /**
242 | * Handling overlord messages
243 | */
244 | open fun onOverlordMessage(message: Any) {
245 |
246 | }
247 |
248 | /**
249 | * Called after onMessage. Useful for saving state.
250 | */
251 | open fun afterMessage() {
252 |
253 | }
254 |
255 | final override fun onReceive(message: Any?) {
256 |
257 | if (message is OverlordMessage) {
258 | onOverlordMessage(message.message)
259 | return
260 | }
261 |
262 | var msg: MagicBotMessage
263 |
264 | when (message) {
265 | //
266 | // General messages
267 | //
268 | is BotMessages.Message -> {
269 | val content = message.message()
270 | when (content) {
271 | //
272 | // Handling Text Message
273 | //
274 | is BotMessages.TextMessage -> {
275 |
276 | //
277 | // Group message filtration
278 | //
279 | if (scope.peer.isGroup) {
280 | if (!enableInGroups) {
281 | return
282 | }
283 | if (onlyWithMentions) {
284 | if (ownNickname != null) {
285 | if (!content.text().toLowerCase().contains("@${ownNickname!!.toLowerCase()}")) {
286 | return
287 | }
288 | } else {
289 | return
290 | }
291 | }
292 | }
293 |
294 | //
295 | // Building Text Message
296 | //
297 | val mText = MagicBotTextMessage(message.peer().toUsable(), message.sender(), message.randomId(),
298 | content.text())
299 | msg = mText
300 |
301 | // Setting Command parameters if needed
302 | val pMsg = ParsedMessage.matchType(content.text())
303 | if (pMsg is MessageCommand) {
304 | mText.command = pMsg.command
305 | mText.commandArgs = pMsg.data
306 | }
307 | }
308 | //
309 | // Handling JSON messages
310 | //
311 | is BotMessages.JsonMessage -> {
312 | try {
313 | val pJson = JSONObject(content.rawJson())
314 | msg = MagicBotJsonMessage(message.peer().toUsable(), message.sender(),
315 | message.randomId(), pJson)
316 | } catch(e: Exception) {
317 | e.printStackTrace()
318 | return
319 | }
320 | }
321 | //
322 | // Handling Document Messages
323 | //
324 | is BotMessages.DocumentMessage -> {
325 | msg = MagicBotDocMessage(message.peer().toUsable(), message.sender(),
326 | message.randomId(), content)
327 | }
328 | //
329 | // Handling Sticker Messages
330 | //
331 | is BotMessages.StickerMessage -> {
332 | msg = MagicBotStickerMessage(message.peer().toUsable(), message.sender(),
333 | message.randomId(), content)
334 | }
335 | //
336 | // Ignoring unknown message
337 | //
338 | else -> {
339 | return
340 | }
341 | }
342 | }
343 | else -> {
344 | return
345 | }
346 | }
347 |
348 | scope.peer = msg.peer
349 | scope.sender = if (msg.sender != null) getUser(msg.sender!!.id()) else null
350 | onMessage(msg)
351 | afterMessage()
352 | }
353 |
354 | /**
355 | * Sending message to Overlord
356 | */
357 | fun sendToOverlord(message: Any) {
358 | scope.overlord?.tell(message, scope.bot.self())
359 | }
360 |
361 | override fun preRestart(reason: Throwable?, message: Option?) {
362 | super.preRestart(reason, message)
363 |
364 | logException(reason)
365 | }
366 | }
367 |
368 |
369 | /**
370 | * Magic Bot Overlord. Actor that is used to receive various updates that is not connected
371 | * to specific conversation.
372 | */
373 | abstract class MagicOverlord(val scope: MagicOverlordScope) :
374 | APITrait by APITraitImpl(scope.bot),
375 | LogTrait by LogTraitImpl(),
376 | BugSnag by BugSnagImpl(),
377 | DispatchTrait by DispatchTraitImpl(),
378 | UntypedActor() {
379 |
380 | init {
381 | initLog(this)
382 | initDispatch(this)
383 | }
384 |
385 | override fun preStart() {
386 | super.preStart()
387 |
388 | Thread.sleep(5000)
389 |
390 | val state = scope.botKeyValue.getJSONValue("overlord_state")
391 | if (state != null) {
392 | onRestoreState(state)
393 | }
394 | }
395 |
396 | open fun onRestoreState(state: JSONObject) {
397 |
398 | }
399 |
400 | open fun onSaveState(state: JSONObject) {
401 |
402 | }
403 |
404 | fun saveState() {
405 | val state = JSONObject()
406 | onSaveState(state)
407 | scope.botKeyValue.setJSONValue("overlord_state", state)
408 | }
409 |
410 | /**
411 | * Called when Web Hook are received
412 | * @param hook WebHook data
413 | */
414 | abstract fun onWebHookReceived(hook: HookData)
415 |
416 | /**
417 | * Sending message to Fork
418 | * @param peer Peer for message
419 | * @param message Message to send
420 | */
421 | fun sendToForks(peer: OutPeer, message: Any) {
422 | scope.rootRef.tell(OverlordMessage(peer, message), self())
423 | }
424 |
425 | override fun onReceive(update: Any?) {
426 | when (update) {
427 | is BotMessages.RawUpdate -> {
428 | if (update.type.isPresent && update.type.get() == "HookData") {
429 | val res = Base64.getDecoder().decode(update.data())
430 | val resS = String(res)
431 | val resJ = JSONObject(resS)
432 | if (resJ.getString("dataType") != "HookData") {
433 | return
434 | }
435 | val data = resJ.getJSONObject("data")
436 | val method = data.getString("method")
437 | val queryString = data.optString("queryString")
438 | val name = data.getString("name")
439 | val body = Base64.getDecoder().decode(data.getString("body"))
440 | val headers = data.getJSONObject("headers")
441 |
442 | var jsonBody: JSONObject? = null
443 | try {
444 | jsonBody = JSONObject(String(body))
445 | } catch(e: Exception) {
446 | // Ignore
447 | }
448 |
449 | onWebHookReceived(HookData(
450 | name = name,
451 | method = method,
452 | query = queryString,
453 | body = body,
454 | jsonBody = jsonBody,
455 | headers = headers))
456 | }
457 | }
458 | }
459 | }
460 |
461 | override fun preRestart(reason: Throwable?, message: Option?) {
462 | super.preRestart(reason, message)
463 |
464 | logException(reason)
465 | }
466 | }
467 |
468 | data class HookData(val name: String, val method: String, val query: String, val body: ByteArray, val jsonBody: JSONObject?, val headers: JSONObject)
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/MagicBotEntities.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework
2 |
3 | import im.actor.bots.BotMessages
4 | import org.json.JSONObject
5 |
6 | //
7 | // Magic Bot Messages
8 | //
9 |
10 | public abstract class MagicBotMessage(val peer: OutPeer, val sender: BotMessages.UserOutPeer?,
11 | val rid: Long) {
12 |
13 | }
14 |
15 | public class MagicBotTextMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long,
16 | val text: String) : MagicBotMessage(peer, sender, rid) {
17 | var command: String? = null
18 | var commandArgs: String? = null
19 | }
20 |
21 | public class MagicBotJsonMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long,
22 | val json: JSONObject) : MagicBotMessage(peer, sender, rid) {
23 |
24 | }
25 |
26 | public class MagicBotDocMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long,
27 | val doc: BotMessages.DocumentMessage) : MagicBotMessage(peer, sender, rid) {
28 |
29 | }
30 |
31 | public class MagicBotStickerMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long,
32 | val sticker: BotMessages.StickerMessage) : MagicBotMessage(peer, sender, rid) {
33 |
34 | }
35 |
36 | //
37 | // User Extensions
38 | //
39 |
40 | var BotMessages.User.isEnterprise: Boolean
41 | get() {
42 | return this.emailContactRecords.size > 0
43 | }
44 | private set(v) {
45 |
46 | }
47 |
48 | //
49 | // Peers and OutPeers
50 | //
51 |
52 | public fun peerFromJson(json: JSONObject): Peer {
53 | val type = json.getString("type")
54 | when (type) {
55 | "group" -> {
56 | return Peer(PeerType.GROUP, json.getInt("id"))
57 | }
58 | "private" -> {
59 | return Peer(PeerType.PRIVATE, json.getInt("id"))
60 | }
61 | else -> {
62 | throw RuntimeException("Unknown type $type")
63 | }
64 | }
65 | }
66 |
67 | public class Peer(val type: PeerType, val id: Int) {
68 |
69 | var isGroup: Boolean
70 | get() {
71 | return type == PeerType.GROUP
72 | }
73 | private set(v) {
74 | }
75 |
76 | var isPrivate: Boolean
77 | get() {
78 | return type == PeerType.PRIVATE
79 | }
80 | private set(v) {
81 | }
82 |
83 | fun toJson(): JSONObject {
84 | val res = JSONObject()
85 | res.put("id", id)
86 | when (type) {
87 | PeerType.GROUP -> {
88 | res.put("type", "group")
89 | }
90 | PeerType.PRIVATE -> {
91 | res.put("type", "private")
92 | }
93 | }
94 | return res
95 | }
96 |
97 | fun toKit(): BotMessages.Peer {
98 | when (type) {
99 | PeerType.PRIVATE -> {
100 | return BotMessages.UserPeer(id)
101 | }
102 | PeerType.GROUP -> {
103 | return BotMessages.GroupPeer(id)
104 | }
105 | }
106 | }
107 |
108 | fun toUniqueId(): String {
109 | when (type) {
110 | PeerType.PRIVATE -> {
111 | return "PRIVATE_$id"
112 | }
113 | PeerType.GROUP -> {
114 | return "GROUP_$id"
115 | }
116 | }
117 | }
118 |
119 | override fun equals(other: Any?): Boolean {
120 | if (this === other) return true
121 | if (other?.javaClass != javaClass) return false
122 |
123 | other as Peer
124 |
125 | if (type != other.type) return false
126 | if (id != other.id) return false
127 |
128 | return true
129 | }
130 |
131 | override fun hashCode(): Int {
132 | var result = type.hashCode()
133 | result += 31 * result + id
134 | return result
135 | }
136 | }
137 |
138 | public fun outPeerFromJson(json: JSONObject): OutPeer {
139 | val type = json.getString("type")
140 | when (type) {
141 | "group" -> {
142 | return OutPeer(PeerType.GROUP, json.getInt("id"), json.getString("accessHash").toLong())
143 | }
144 | "private" -> {
145 | return OutPeer(PeerType.PRIVATE, json.getInt("id"), json.getString("accessHash").toLong())
146 | }
147 | else -> {
148 | throw RuntimeException("Unknown type $type")
149 | }
150 | }
151 | }
152 |
153 | public fun BotMessages.OutPeer.toUsable(): OutPeer {
154 | if (this is BotMessages.UserOutPeer) {
155 | return OutPeer(PeerType.PRIVATE, id(), accessHash())
156 | } else if (this is BotMessages.GroupOutPeer) {
157 | return OutPeer(PeerType.GROUP, id(), accessHash())
158 | } else {
159 | throw RuntimeException("Unknown type")
160 | }
161 | }
162 |
163 | public class OutPeer(val type: PeerType, val id: Int, val accessHash: Long) {
164 |
165 | var isGroup: Boolean
166 | get() {
167 | return type == PeerType.GROUP
168 | }
169 | private set(v) {
170 | }
171 |
172 | var isPrivate: Boolean
173 | get() {
174 | return type == PeerType.PRIVATE
175 | }
176 | private set(v) {
177 | }
178 |
179 | fun toJson(): JSONObject {
180 | val res = JSONObject()
181 | res.put("id", id)
182 | res.put("accessHash", "$accessHash")
183 | when (type) {
184 | PeerType.GROUP -> {
185 | res.put("type", "group")
186 | }
187 | PeerType.PRIVATE -> {
188 | res.put("type", "private")
189 | }
190 | }
191 | return res
192 | }
193 |
194 | fun toPeer(): Peer {
195 | return Peer(type, id)
196 | }
197 |
198 | fun toKit(): BotMessages.OutPeer {
199 | when (type) {
200 | PeerType.PRIVATE -> {
201 | return BotMessages.UserOutPeer(id, accessHash)
202 | }
203 | PeerType.GROUP -> {
204 | return BotMessages.GroupOutPeer(id, accessHash)
205 | }
206 | }
207 | }
208 |
209 | fun toUniqueId(): String {
210 | when (type) {
211 | PeerType.PRIVATE -> {
212 | return "PRIVATE_$id"
213 | }
214 | PeerType.GROUP -> {
215 | return "GROUP_$id"
216 | }
217 | }
218 | }
219 |
220 | override fun equals(other: Any?): Boolean {
221 | if (this === other) return true
222 | if (other?.javaClass != javaClass) return false
223 |
224 | other as OutPeer
225 |
226 | if (type != other.type) return false
227 | if (id != other.id) return false
228 | if (accessHash != other.accessHash) return false
229 |
230 | return true
231 | }
232 |
233 | override fun hashCode(): Int {
234 | var result = type.hashCode()
235 | result += 31 * result + id
236 | result += 31 * result + accessHash.hashCode()
237 | return result
238 | }
239 | }
240 |
241 | public enum class PeerType(val id: Int) {
242 | PRIVATE(0), GROUP(1)
243 | }
244 |
245 |
246 | public fun BotMessages.Peer.toUsable(): Peer {
247 | if (this is BotMessages.UserPeer) {
248 | return Peer(PeerType.PRIVATE, id())
249 | } else if (this is BotMessages.GroupPeer) {
250 | return Peer(PeerType.GROUP, id())
251 | } else {
252 | throw RuntimeException("Unknown type")
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/MagicBotFarm.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework
2 |
3 | import akka.actor.ActorSystem
4 | import akka.actor.Props
5 | import im.actor.botkit.RemoteBot
6 | import java.util.*
7 | import kotlin.reflect.KClass
8 |
9 | class BotFarm(val name: String) {
10 |
11 | var endpoint = RemoteBot.DefaultEndpoint()
12 | val system = ActorSystem.create(name)
13 | val bots = ArrayList()
14 |
15 | init {
16 |
17 | }
18 |
19 | fun bot(clazz: KClass, init: BotDescription.() -> Unit) {
20 | val b = BotDescription(clazz as KClass)
21 | b.init()
22 | bots.add(b)
23 | }
24 |
25 | fun startFarm() {
26 |
27 | for (b in bots) {
28 | var config = MagicBotConfig(b.name!!, b.clazz.java, b.overlordClazz, b.token!!,
29 | endpoint, b.traceHook)
30 | system.actorOf(Props.create(MagicalRemoteBot::class.java, config), b.name)
31 | }
32 |
33 | system.awaitTermination()
34 | }
35 | }
36 |
37 | class BotDescription(val clazz: KClass) {
38 | var name: String? = null
39 | var token: String? = null
40 | var traceHook: String? = null
41 | var overlordClazz: Class<*>? = null
42 | }
43 |
44 | public fun farm(name: String, init: BotFarm.() -> Unit) {
45 | val res = BotFarm(name)
46 | res.init()
47 | res.startFarm()
48 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/i18n/I18NEngine.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.i18n;
2 |
3 | import java.io.IOException;
4 | import java.util.ArrayList;
5 | import java.util.HashMap;
6 | import java.util.Properties;
7 | import java.util.Random;
8 |
9 | public class I18NEngine {
10 |
11 | private final Random random = new Random();
12 | private HashMap> strings = new HashMap<>();
13 |
14 | public I18NEngine(String fileName) throws IOException {
15 |
16 | Properties properties = new Properties();
17 | properties.load(getClass().getClassLoader().getResourceAsStream(fileName));
18 |
19 | for (String key : properties.stringPropertyNames()) {
20 | String value = new String(properties.getProperty(key).getBytes("ISO-8859-1"), "UTF-8");
21 |
22 | String[] keyParts = key.split("\\.");
23 | try {
24 | Integer.parseInt(keyParts[keyParts.length - 1]);
25 | key = "";
26 | for (int i = 0; i < keyParts.length - 1; i++) {
27 | if (key.length() > 0) {
28 | key += ".";
29 | }
30 | key += keyParts[i];
31 | }
32 | } catch (Exception e) {
33 | // Expected
34 | }
35 |
36 | if (strings.containsKey(key)) {
37 | strings.get(key).add(value);
38 | } else {
39 | ArrayList s = new ArrayList<>();
40 | s.add(value);
41 | strings.put(key, s);
42 | }
43 | }
44 | }
45 |
46 | public String pick(String key) {
47 | ArrayList s = strings.get(key);
48 | int index;
49 | synchronized (random) {
50 | index = random.nextInt(s.size());
51 | }
52 | return s.get(index);
53 | }
54 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/i18n/Strings.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.i18n;
2 |
3 | import java.util.Random;
4 |
5 | public class Strings {
6 | public static final String[] UNKNOWN_MESSAGES = {
7 | "Command is invalid. Say what?",
8 | "Command is invalid. I really didn't get it...",
9 | "Command is invalid. What do you mean?",
10 | "Command is invalid. Please, say it again in a good way.",
11 | };
12 | private static Random random = new Random();
13 |
14 | public static String unknown() {
15 | return UNKNOWN_MESSAGES[random.nextInt(UNKNOWN_MESSAGES.length)];
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/parser/MessageCommand.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.parser;
2 |
3 | import java.util.List;
4 |
5 | public class MessageCommand extends ParsedMessage {
6 |
7 | private String command;
8 | private List args;
9 | private String data;
10 |
11 | public MessageCommand(String command, List args, String data) {
12 | this.command = command;
13 | this.data = data;
14 | this.args = args;
15 | }
16 |
17 | public String getCommand() {
18 | return command;
19 | }
20 |
21 | public String getData() {
22 | return data;
23 | }
24 |
25 | public List getArgs() {
26 | return args;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/parser/MessageText.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.parser;
2 |
3 | public class MessageText extends ParsedMessage {
4 |
5 | final private String text;
6 |
7 | public MessageText(String text) {
8 | this.text = text;
9 | }
10 |
11 | public String getText() {
12 | return text;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/parser/ParsedMessage.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.parser;
2 |
3 | import java.util.ArrayList;
4 |
5 | public abstract class ParsedMessage {
6 | public static ParsedMessage matchType(String message) {
7 | message = message.trim();
8 | if (message.startsWith("/")) {
9 | String[] data = ParsingUtils.splitFirstWord(message);
10 | String command = data[0].substring(1);
11 | ArrayList args = new ArrayList();
12 | String text = "";
13 | if (data.length == 2) {
14 | text = data[1];
15 | }
16 |
17 | try {
18 | if (command.contains("(") || command.endsWith(")")) {
19 | String container = command.substring(command.indexOf('(') + 1, command.length() - 1);
20 | if (container.contains("(") || container.contains(")")) {
21 | throw new RuntimeException();
22 | }
23 | for (String s : container.split(",")) {
24 | args.add(s.trim());
25 | }
26 | command = command.substring(0, command.indexOf('('));
27 | }
28 | } catch (Exception e) {
29 | e.printStackTrace();
30 | }
31 |
32 |
33 | return new MessageCommand(command, args, text);
34 | } else {
35 | return new MessageText(message);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/parser/ParsingUtils.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.parser;
2 |
3 | public class ParsingUtils {
4 |
5 | public static String[] splitFirstWord(String text) {
6 | return text.trim().split(" ", 2);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/persistence/KotlinExtensions.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.persistence
2 |
3 | import akka.util.Timeout
4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5 | import com.fasterxml.jackson.module.kotlin.readValue
6 | import shardakka.keyvalue.SimpleKeyValueJava
7 | import java.io.ByteArrayOutputStream
8 | import java.util.concurrent.TimeUnit
9 |
10 | fun ServerKeyValue.setDataClass(key: String, obj: T?) {
11 | if (obj == null) {
12 | setStringValue(key, null)
13 | } else {
14 | val output = ByteArrayOutputStream()
15 | val generator = jacksonObjectMapper().jsonFactory.createGenerator(output)
16 | generator.writeObject(obj)
17 | val str = String(output.toByteArray())
18 | setStringValue(key, str)
19 | }
20 | }
21 |
22 | inline fun ServerKeyValue.getDataClass(key: String): T? {
23 | val str = getStringValue(key)
24 | if (str == null) {
25 | return null
26 | } else {
27 | return jacksonObjectMapper().readValue(str)
28 | }
29 | }
30 |
31 | fun SimpleKeyValueJava.get(key: String): T? {
32 | val res = syncGet(key, Timeout.apply(10, TimeUnit.SECONDS))
33 | if (res.isPresent) {
34 | return res.get()
35 | } else {
36 | return null
37 | }
38 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/persistence/MagicBotPersistence.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.persistence
2 |
3 | import im.actor.bots.framework.MagicBotFork
4 | import im.actor.bots.framework.MagicForkScope
5 | import org.json.JSONObject
6 |
7 | abstract class MagicPersistentBot(scope: MagicForkScope) : MagicBotFork(scope) {
8 |
9 | // private val stateKeyValue: SimpleKeyValueJava =
10 | // ShardakkaExtension.get(context().system()).simpleKeyValue("\$${scope.name}_state_" + scope.peer.toUniqueId()).asJava()
11 |
12 | override fun preStart() {
13 | super.preStart()
14 |
15 | // val res = stateKeyValue.get("actor_state")
16 | // if (res != null) {
17 | // val state = JSONObject(res)
18 | // val internalState = state.getJSONObject("state")
19 | // onRestoreState(internalState)
20 | // }
21 | }
22 |
23 | open fun onRestoreState(state: JSONObject) {
24 |
25 | }
26 |
27 | open fun onSaveState(state: JSONObject) {
28 |
29 | }
30 |
31 | override fun afterMessage() {
32 | // saveState()
33 | }
34 |
35 | fun saveState() {
36 | // val res = JSONObject()
37 | // val internalState = JSONObject()
38 | // onSaveState(internalState)
39 | // res.put("state", internalState)
40 | // stateKeyValue.syncUpsert("actor_state", res.toString())
41 | }
42 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/persistence/ServerKeyValue.java:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.persistence;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.jetbrains.annotations.Nullable;
5 | import org.json.JSONObject;
6 |
7 | import java.util.HashMap;
8 | import java.util.concurrent.TimeUnit;
9 |
10 | import im.actor.botkit.RemoteBot;
11 | import im.actor.bots.BotMessages;
12 | import scala.Option;
13 | import scala.concurrent.Await;
14 | import scala.concurrent.duration.Duration;
15 |
16 | import static scala.compat.java8.JFunction.proc;
17 |
18 | public class ServerKeyValue {
19 |
20 | private static final String KEY_SPACE = "default";
21 |
22 | private String keySpace;
23 | private RemoteBot remoteBot;
24 | private HashMap cachedValues = new HashMap();
25 |
26 | public ServerKeyValue(@NotNull RemoteBot remoteBot) {
27 | this(remoteBot, KEY_SPACE);
28 | }
29 |
30 | public ServerKeyValue(@NotNull RemoteBot remoteBot, @NotNull String keySpace) {
31 | this.keySpace = keySpace;
32 | this.remoteBot = remoteBot;
33 | }
34 |
35 | public void setStringValue(@NotNull String key, @Nullable String value) {
36 | cachedValues.put(key, value);
37 | remoteBot.requestSetValue(keySpace, key, value).foreach(proc(s -> {
38 |
39 | }), remoteBot.context().dispatcher());
40 | }
41 |
42 | @Nullable
43 | public String getStringValue(@NotNull String key) throws Exception {
44 | if (cachedValues.containsKey(key)) {
45 | return cachedValues.get(key);
46 | }
47 | BotMessages.Container> res = Await.result(remoteBot.requestGetValue(keySpace, key), Duration.create(60, TimeUnit.SECONDS));
48 | if (res.value().nonEmpty()) {
49 | String val = res.value().get();
50 | cachedValues.put(key, val);
51 | return val;
52 | }
53 | return null;
54 | }
55 |
56 | public void setDoubleValue(@NotNull String key, @Nullable Double value) {
57 | if (value != null) {
58 | setStringValue(key, Double.toString(value));
59 | } else {
60 | setStringValue(key, null);
61 | }
62 | }
63 |
64 | @Nullable
65 | public Double getDoubleValue(@NotNull String key) throws Exception {
66 | String res = getStringValue(key);
67 | if (res != null) {
68 | return Double.parseDouble(res);
69 | } else {
70 | return null;
71 | }
72 | }
73 |
74 | public void setIntValue(@NotNull String key, @Nullable Integer value) {
75 | if (value != null) {
76 | setStringValue(key, Integer.toString(value));
77 | } else {
78 | setStringValue(key, null);
79 | }
80 | }
81 |
82 | @Nullable
83 | public Integer getIntValue(@NotNull String key) throws Exception {
84 | String res = getStringValue(key);
85 | if (res != null) {
86 | return Integer.parseInt(res);
87 | } else {
88 | return null;
89 | }
90 | }
91 |
92 | public void setBoolValue(@NotNull String key, @Nullable Boolean value) {
93 | if (value != null) {
94 | setStringValue(key, Boolean.toString(value));
95 | } else {
96 | setStringValue(key, null);
97 | }
98 | }
99 |
100 | @Nullable
101 | public Boolean getBoolValue(@NotNull String key) throws Exception {
102 | String res = getStringValue(key);
103 | if (res != null) {
104 | return Boolean.parseBoolean(res);
105 | } else {
106 | return null;
107 | }
108 | }
109 |
110 | public boolean getBoolValue(@NotNull String key, boolean value) throws Exception {
111 | String res = getStringValue(key);
112 | if (res != null) {
113 | return Boolean.parseBoolean(res);
114 | } else {
115 | return value;
116 | }
117 | }
118 |
119 | public void setLongValue(@NotNull String key, @Nullable Long value) {
120 | if (value != null) {
121 | setStringValue(key, Long.toString(value));
122 | } else {
123 | setStringValue(key, null);
124 | }
125 | }
126 |
127 | @Nullable
128 | public Long getLongValue(@NotNull String key) throws Exception {
129 | String res = getStringValue(key);
130 | if (res != null) {
131 | return Long.parseLong(res);
132 | } else {
133 | return null;
134 | }
135 | }
136 |
137 | public void setJSONValue(@NotNull String key, @Nullable JSONObject value) {
138 | if (value != null) {
139 | setStringValue(key, value.toString());
140 | } else {
141 | setStringValue(key, null);
142 | }
143 | }
144 |
145 | @Nullable
146 | public JSONObject getJSONValue(@NotNull String key) throws Exception {
147 | String res = getStringValue(key);
148 | if (res != null) {
149 | return new JSONObject(res);
150 | } else {
151 | return null;
152 | }
153 | }
154 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/stateful/Expect.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.stateful
2 |
3 | import im.actor.bots.BotMessages
4 | import im.actor.bots.framework.*
5 | import im.actor.bots.framework.traits.ModernMessage
6 | import org.json.JSONObject
7 | import java.util.*
8 |
9 | abstract class Expect(val stateName: String, val parent: Expect?) : ExpectContainer {
10 |
11 | var defaultState: String? = null
12 | var child = HashMap()
13 |
14 | private var beforeClosure: (ExpectContext.() -> Unit)? = null
15 |
16 | fun before(before: (ExpectContext.() -> Unit)?) {
17 | beforeClosure = before
18 | }
19 |
20 | open fun onReceived(context: ExpectContext) {
21 |
22 | }
23 |
24 | open fun onBefore(context: ExpectContext) {
25 | if (beforeClosure != null) {
26 | applyContext(context, beforeClosure!!)
27 | }
28 | }
29 |
30 | override fun addChild(expect: Expect) {
31 | if (defaultState == null) {
32 | defaultState = expect.stateName
33 | }
34 | child.put(expect.stateName, expect)
35 | }
36 |
37 | protected fun applyContext(context: ExpectContext, closure: ExpectContext.() -> Unit) {
38 | context.closure()
39 | }
40 |
41 | protected fun applyContext(context: ExpectContext, closure: ExpectContext.() -> Boolean): Boolean {
42 | return context.closure()
43 | }
44 |
45 | fun fullName(): String {
46 | var res = stateName
47 | if (parent != null) {
48 | res = parent.fullName() + "." + res
49 | }
50 | return res
51 | }
52 |
53 | override fun getContainer(): Expect? {
54 | return this
55 | }
56 | }
57 |
58 | interface ExpectContext {
59 | var body: MagicBotMessage?
60 | get
61 |
62 | fun goto(stateId: String)
63 | fun tryGoto(stateId: String): Boolean
64 | fun gotoParent(level: Int)
65 | fun gotoParent()
66 | fun log(text: String)
67 | fun sendText(text: String)
68 | fun sendJson(dataType: String, json: JSONObject)
69 | fun sendModernText(message: ModernMessage)
70 | }
71 |
72 | interface ExpectContainer {
73 | fun addChild(expect: Expect)
74 | fun getContainer(): Expect?
75 | }
76 |
77 | interface ExpectCommandContainer : ExpectContainer {
78 |
79 | }
80 |
81 | var ExpectContext.text: String
82 | get() {
83 | if (body is MagicBotTextMessage) {
84 | return (body as MagicBotTextMessage).text!!
85 | }
86 | throw RuntimeException()
87 | }
88 | private set(v) {
89 |
90 | }
91 |
92 | var ExpectContext.isText: Boolean
93 | get() {
94 | if (body is MagicBotTextMessage) {
95 | return (body as MagicBotTextMessage).command == null
96 | }
97 | return false
98 | }
99 | private set(v) {
100 |
101 | }
102 |
103 | var ExpectContext.isCommand: Boolean
104 | get() {
105 | if (body is MagicBotTextMessage) {
106 | return (body as MagicBotTextMessage).command != null
107 | }
108 | return false
109 | }
110 | private set(v) {
111 |
112 | }
113 |
114 | var ExpectContext.command: String?
115 | get() {
116 | if (body is MagicBotTextMessage) {
117 | return (body as MagicBotTextMessage).command
118 | }
119 | return null
120 | }
121 | private set(v) {
122 |
123 | }
124 |
125 | var ExpectContext.commandArgs: String?
126 | get() {
127 | if (body is MagicBotTextMessage) {
128 | return (body as MagicBotTextMessage).commandArgs
129 | }
130 | return null
131 | }
132 | private set(v) {
133 |
134 | }
135 |
136 | var ExpectContext.isCancel: Boolean
137 | get() {
138 | return isCommand && command == "cancel"
139 | }
140 | private set(v) {
141 |
142 | }
143 |
144 | var ExpectContext.isDoc: Boolean
145 | get() {
146 | return body is MagicBotDocMessage
147 | }
148 | private set(v) {
149 |
150 | }
151 |
152 | var ExpectContext.doc: BotMessages.DocumentMessage
153 | get() {
154 | if (body is MagicBotDocMessage) {
155 | return (body as MagicBotDocMessage).doc
156 | }
157 | throw RuntimeException()
158 | }
159 | private set(v) {
160 |
161 | }
162 |
163 |
164 | var ExpectContext.isSticker: Boolean
165 | get() {
166 | return body is MagicBotStickerMessage
167 | }
168 | private set(v) {
169 |
170 | }
171 |
172 | var ExpectContext.sticker: BotMessages.StickerMessage
173 | get() {
174 | if (body is MagicBotStickerMessage) {
175 | return (body as MagicBotStickerMessage).sticker
176 | }
177 | throw RuntimeException()
178 | }
179 | private set(v) {
180 |
181 | }
182 |
183 |
184 | var ExpectContext.isPhoto: Boolean
185 | get() {
186 | if (body is MagicBotDocMessage) {
187 | val doc = body as MagicBotDocMessage
188 | if (doc.doc.ext.isPresent && doc.doc.ext.get() is BotMessages.DocumentExPhoto) {
189 | return true
190 | }
191 | }
192 | return false
193 | }
194 | private set(v) {
195 |
196 | }
197 |
198 | var ExpectContext.responseJson: JSONObject
199 | get() {
200 | if (body is MagicBotJsonMessage) {
201 | return (body as MagicBotJsonMessage).json
202 | }
203 | throw RuntimeException()
204 | }
205 | private set(v) {
206 |
207 | }
208 |
209 | var ExpectContext.isJson: Boolean
210 | get() {
211 | return body is MagicBotJsonMessage
212 | }
213 | private set(v) {
214 |
215 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectCommands.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.stateful
2 |
3 | import im.actor.bots.framework.MagicBotTextMessage
4 |
5 | class ExpectCommands(name: String, parent: Expect?) : Expect(name, parent), ExpectCommandContainer {
6 |
7 | private var unknownCommand: (ExpectContext.() -> Unit)? = null
8 | private var notACommand: (ExpectContext.() -> Unit)? = null
9 |
10 | override fun onReceived(context: ExpectContext) {
11 |
12 | if (context.body is MagicBotTextMessage) {
13 | val textMessage = context.body as MagicBotTextMessage
14 | if (textMessage.command != null) {
15 | if (child.containsKey("/" + textMessage.command!!)) {
16 |
17 | context.goto("/" + textMessage.command!!)
18 | return
19 | } else {
20 |
21 | // Unknown command
22 | if (unknownCommand != null) {
23 | applyContext(context, unknownCommand!!)
24 | } else {
25 | context.tryGoto("default")
26 | }
27 |
28 | return
29 | }
30 | }
31 | }
32 |
33 | if (notACommand != null) {
34 | applyContext(context, notACommand!!)
35 | } else {
36 | context.tryGoto("default")
37 | }
38 | }
39 |
40 | override fun getContainer(): ExpectCommands {
41 | return this
42 | }
43 | }
44 |
45 | class ExpectCommand(name: String, parent: Expect?) : Expect(name, parent) {
46 |
47 |
48 | override fun onReceived(context: ExpectContext) {
49 |
50 | }
51 | }
52 |
53 | public fun ExpectCommandContainer.command(name: String, init: (ExpectCommand.() -> Unit)): ExpectCommand {
54 | val res = ExpectCommand(name, getContainer())
55 | addChild(res)
56 | res.init()
57 | return res
58 | }
59 |
60 | public fun ExpectContainer.oneShot(name: String, init: (ExpectContext.() -> Unit)): ExpectCommand {
61 | val res = ExpectCommand(name, getContainer())
62 | res.before {
63 | init()
64 | gotoParent()
65 | }
66 | addChild(res)
67 | return res
68 | }
69 |
70 | public fun ExpectCommandContainer.expectCommands(init: (ExpectCommands.() -> Unit)): ExpectCommands {
71 | return expectCommands("main", init)
72 | }
73 |
74 | public fun ExpectCommandContainer.expectCommands(name: String, init: (ExpectCommands.() -> Unit)): ExpectCommands {
75 | val res = ExpectCommands(name, getContainer())
76 | addChild(res)
77 | res.init()
78 | return res
79 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectInput.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.stateful
2 |
3 | open class ExpectInput(stateName: String, parent: Expect?) : Expect(stateName, parent) {
4 |
5 | protected var receivedClosure: (ExpectContext.() -> Unit)? = null
6 |
7 | fun received(receive: (ExpectContext.() -> Unit)?) {
8 | receivedClosure = receive
9 | }
10 |
11 | override fun onReceived(context: ExpectContext) {
12 | if (receivedClosure != null) {
13 | applyContext(context, receivedClosure!!)
14 | }
15 | }
16 | }
17 |
18 | open class ExpectValidatedInput(stateName: String, parent: Expect?) : ExpectInput(stateName, parent) {
19 |
20 | private var validateClosure: (ExpectContext.() -> Boolean)? = null
21 |
22 | fun validate(validate: (ExpectContext.() -> Boolean)?) {
23 | validateClosure = validate
24 | }
25 |
26 | override fun onReceived(context: ExpectContext) {
27 | if (applyContext(context, validateClosure!!)) {
28 | if (receivedClosure != null) {
29 | applyContext(context, receivedClosure!!)
30 | }
31 | }
32 | }
33 | }
34 |
35 | fun ExpectContainer.expectInput(name: String, init: ((ExpectValidatedInput.() -> Unit))): ExpectValidatedInput {
36 | val res = ExpectValidatedInput(name, getContainer())
37 | addChild(res)
38 | res.init()
39 | return res
40 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectRaw.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.stateful
2 |
3 | class ExpectRaw(stateName: String, parent: Expect?) : Expect(stateName, parent) {
4 |
5 | private var receiveClosure: (ExpectContext.() -> Unit)? = null
6 |
7 | fun received(receive: (ExpectContext.() -> Unit)?) {
8 | receiveClosure = receive
9 | }
10 |
11 | override fun onReceived(context: ExpectContext) {
12 | if (receiveClosure != null) {
13 | applyContext(context, receiveClosure!!)
14 | }
15 | }
16 | }
17 |
18 | fun ExpectContainer.raw(name: String, init: ((ExpectRaw.() -> Unit))): ExpectRaw {
19 | val res = ExpectRaw(name, getContainer())
20 | addChild(res)
21 | res.init()
22 | return res
23 | }
24 |
25 | fun ExpectContainer.static(name: String, init: ((ExpectContext.() -> Unit))): ExpectRaw {
26 | val res = ExpectRaw(name, getContainer())
27 | addChild(res)
28 | res.before {
29 | init()
30 | }
31 | return res
32 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/stateful/MagicBotStateful.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.stateful
2 |
3 | import im.actor.bots.framework.MagicBotMessage
4 | import im.actor.bots.framework.MagicForkScope
5 | import im.actor.bots.framework.i18n.Strings
6 | import im.actor.bots.framework.persistence.MagicPersistentBot
7 | import org.json.JSONObject
8 |
9 | abstract class MagicStatefulBot(scope: MagicForkScope) : MagicPersistentBot(scope), ExpectContext, ExpectCommandContainer {
10 |
11 | var enablePersistent = false
12 | var currentBody: MagicBotMessage? = null
13 | var root: Expect? = null
14 | var currentState: Expect? = null
15 |
16 | override fun preStart() {
17 | configure()
18 | if (root == null) {
19 | throw RuntimeException("Root state not installed")
20 | }
21 | // Initial state will set in onRestoreState
22 | super.preStart()
23 | if (currentState == null) {
24 | currentState = root
25 | }
26 | }
27 |
28 | override fun onSaveState(state: JSONObject) {
29 | if (enablePersistent) {
30 | state.put("#state_id", currentState!!.fullName())
31 | }
32 | }
33 |
34 | override fun onRestoreState(state: JSONObject) {
35 | if (enablePersistent) {
36 | val id = state.optString("#state_id", root!!.fullName())
37 | log("Loaded name $id")
38 | val dest = findExpect(id, root!!)
39 | if (dest != null) {
40 | log("Found dest ${dest.fullName()}")
41 | currentState = dest
42 | } else {
43 | log("Unable to found")
44 | currentState = root
45 | }
46 | } else {
47 | currentState = root
48 | }
49 |
50 | }
51 |
52 | abstract fun configure()
53 |
54 | /**
55 | * Enabling Simple Mode. Useful for simple geeky bots that works only in private chats
56 | * and consist of just list of commands.
57 | */
58 | fun enableSimpleMode(hint: String, unknown: String? = null) {
59 |
60 | enableInGroups = false
61 | enablePersistent = true
62 |
63 | //
64 | // Enable error on unknown command
65 | //
66 |
67 | oneShot("default") {
68 | if (unknown != null) {
69 | sendText(localized(unknown))
70 | } else {
71 | sendText(Strings.unknown())
72 | }
73 | }
74 |
75 | //
76 | // Hint
77 | //
78 |
79 | oneShot("/start") {
80 | sendText(hint)
81 | }
82 | }
83 |
84 | override fun onMessage(message: MagicBotMessage) {
85 | currentBody = message
86 | currentState!!.onReceived(this)
87 | }
88 |
89 | // Context
90 |
91 | override var body: MagicBotMessage?
92 | get() {
93 | return currentBody
94 | }
95 | set(value) {
96 | }
97 |
98 | override fun goto(stateId: String) {
99 | val n = findExpect(stateId, currentState!!) ?: throw RuntimeException("Unable to find $stateId")
100 | currentState = n
101 | log("goto: ${currentState!!.fullName()}")
102 | n.onBefore(this)
103 | }
104 |
105 | override fun tryGoto(stateId: String): Boolean {
106 | val n = findExpect(stateId, currentState!!)
107 | if (n != null) {
108 | currentState = n
109 | log("tryGoto: ${currentState!!.fullName()}")
110 | n.onBefore(this)
111 | return true
112 | } else {
113 | return false
114 | }
115 | }
116 |
117 | private fun findExpect(stateId: String, start: Expect): Expect? {
118 | var parts = stateId.split(".")
119 | // Finding with starting included
120 | return findExpectUp(parts, start)
121 | // if (res != null) {
122 | // return res
123 | // }
124 | //// // Finding in all children
125 | //// for (c in start.child.values) {
126 | //// val res = findExpectFrom(parts, c)
127 | //// if (res != null) {
128 | //// return res
129 | //// }
130 | //// }
131 | ////
132 | //// // Finding in root
133 | //// val resRoot = findExpectFrom(parts, root!!)
134 | //// if (resRoot != null) {
135 | //// return resRoot
136 | //// }
137 | ////
138 | //// // Finding in root children
139 | //// for (c in root!!.child.values) {
140 | //// val res = findExpectFrom(parts, c)
141 | //// if (res != null) {
142 | //// return res
143 | //// }
144 | //// }
145 | //
146 | // // Nothing found
147 | // return null
148 | }
149 |
150 | private fun findExpectUp(stateIds: List, start: Expect): Expect? {
151 | val res = findExpectFrom(stateIds, start)
152 | if (res != null) {
153 | return res
154 | }
155 | if (start.parent != null) {
156 | return findExpectUp(stateIds, start.parent)
157 | } else {
158 | return null
159 | }
160 | }
161 |
162 | private fun findExpectFrom(stateIds: List, start: Expect): Expect? {
163 | if (stateIds.count() == 0) {
164 | return start
165 | }
166 | val id = stateIds[0]
167 | if (start.stateName == id) {
168 | if (stateIds.count() == 1) {
169 | return start
170 | }
171 | for (i in start.child.values) {
172 | val res = findExpectFrom(stateIds.drop(1).toList(), i)
173 | if (res != null) {
174 | return res
175 | }
176 | }
177 | }
178 | for (i in start.child.values) {
179 | if (i.stateName == id) {
180 | return findExpectFrom(stateIds.drop(1).toList(), i)
181 | }
182 | }
183 | // if (start.stateName == id) {
184 | // if (stateIds.count() == 1) {
185 | // return start
186 | // }
187 | // for (i in start.child.values) {
188 | // val res = findExpectFrom(stateIds.drop(1).toList(), i)
189 | // if (res != null) {
190 | // return res
191 | // }
192 | // }
193 | // }
194 |
195 | return null
196 | }
197 |
198 | override fun gotoParent() {
199 | if (currentState != null && currentState!!.parent != null) {
200 | currentState = currentState!!.parent!!
201 | log("GotoParent: ${currentState!!.fullName()}")
202 | currentState!!.onBefore(this)
203 | } else {
204 | throw RuntimeException("${currentState!!.fullName()} doesn't have parent")
205 | }
206 | }
207 |
208 | override fun gotoParent(level: Int) {
209 |
210 | }
211 |
212 | override fun log(text: String) {
213 | v(text)
214 | }
215 |
216 | // Container
217 |
218 | override fun addChild(expect: Expect) {
219 | getContainer().addChild(expect)
220 | }
221 |
222 | override fun getContainer(): ExpectCommands {
223 | if (root != null) {
224 | if (root !is ExpectCommands) {
225 | throw RuntimeException("Root is not commands container")
226 | }
227 | } else {
228 | root = ExpectCommands("main", null)
229 | }
230 | return root as ExpectCommands
231 | }
232 |
233 | protected fun formSend(name: String, toSend: String): String {
234 | return "[$name](send:$toSend)"
235 | }
236 |
237 | protected fun formSend(command: String): String {
238 | return formSend(command, command)
239 | }
240 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/APITrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import im.actor.botkit.RemoteBot
4 | import im.actor.bots.BotMessages
5 | import im.actor.bots.framework.OutPeer
6 | import org.json.JSONObject
7 | import scala.Option
8 | import scala.concurrent.Await
9 | import scala.concurrent.duration.Duration
10 | import java.util.*
11 | import java.util.concurrent.TimeUnit
12 |
13 | interface APITrait {
14 |
15 | //
16 | // Messaging
17 | //
18 |
19 | fun sendText(peer: OutPeer, text: String)
20 |
21 | fun sendJson(peer: OutPeer, dataType: String, json: JSONObject)
22 |
23 | fun sendModernText(peer: OutPeer, message: ModernMessage)
24 |
25 | fun findUser(query: String): BotMessages.User?
26 |
27 | fun getUser(uid: Int): BotMessages.User
28 |
29 | fun getGroup(gid: Int): BotMessages.Group
30 |
31 | fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup?
32 |
33 | fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean
34 |
35 | //
36 | // Managing Hooks
37 | //
38 |
39 | fun createHook(hookName: String): String?
40 |
41 | //
42 | // Super Bot methods
43 | //
44 |
45 | fun createBot(userName: String, name: String): BotMessages.BotCreated?
46 | fun changeUserName(uid: Int, name: String): Boolean
47 | fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean
48 | fun changeUserAbout(uid: Int, name: String): Boolean
49 | fun userIsAdmin(uid: Int): Boolean
50 |
51 | fun createStickerPack(ownerUserId: Int): Int
52 | fun addSticker(ownerUserId: Int, packId: Int, emoji: Optional, image128: ByteArray, w128: Int, h128: Int, image256: ByteArray, w256: Int, h256: Int, image512: ByteArray, w512: Int, h512: Int): Boolean
53 | fun showStickerPacks(ownerUserId: Int): List
54 | fun showStickers(ownerUserId: Int, packId: Int): List
55 | fun deleteSticker(ownerUserId: Int, packId: Int, stickerId: Int): Boolean
56 | fun makeStickerPackDefault(userId: Int, packId: Int): Boolean
57 | fun unmakeStickerPackDefault(userId: Int, packId: Int): Boolean
58 |
59 | fun getFile(fileId: Long, accessHash: Long): ByteArray
60 | }
61 |
62 |
63 | class ModernMessage(val compatText: String) {
64 | var text: String? = null
65 | var paragraphStyle: ParagraphStyle? = null
66 | var attaches = ArrayList()
67 | }
68 |
69 | class ParagraphStyle {
70 | var showParagraph: Boolean = false
71 | var paragraphColor: Color? = null
72 | var backgroundColor: Color? = null
73 | }
74 |
75 | class ModernAttach {
76 | var text: String? = null
77 | var title: String? = null
78 | var titleUrl: String? = null
79 | var paragraphStyle: ParagraphStyle? = null
80 | var fields = ArrayList()
81 | }
82 |
83 | class ModernField(val name: String, val value: String, val isShort: Boolean) {
84 | }
85 |
86 | sealed class Color {
87 | class RGB(val number: Int) : Color()
88 |
89 | object Red : Color()
90 |
91 | object Green : Color()
92 |
93 | object Yellow : Color()
94 | }
95 |
96 | interface APITraitScoped : APITrait {
97 | fun sendText(text: String)
98 | fun sendJson(dataType: String, json: JSONObject)
99 | fun sendModernText(message: ModernMessage)
100 | }
101 |
102 | open class APITraitImpl(val bot: RemoteBot) : APITrait {
103 |
104 | override fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean {
105 | try {
106 | Await.result(bot.requestInviteUser(group, user), Duration.create(50, TimeUnit.SECONDS))
107 | return true
108 | } catch(e: Exception) {
109 | return false
110 | }
111 | }
112 |
113 | override fun sendText(peer: OutPeer, text: String) {
114 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.TextMessage(text, Option.empty()))
115 | }
116 |
117 | override fun sendJson(peer: OutPeer, dataType: String, data: JSONObject) {
118 | val jsonMsg = JSONObject()
119 | jsonMsg.put("dataType", dataType);
120 | jsonMsg.put("data", data)
121 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.JsonMessage(jsonMsg.toString()))
122 | }
123 |
124 | override fun sendModernText(peer: OutPeer, message: ModernMessage) {
125 | val paragraphStyle = convertParagraphStyle(message.paragraphStyle)
126 | var attachesList = ArrayList()
127 | for (m in message.attaches) {
128 | var attrs = ArrayList()
129 | for (f in m.fields) {
130 | attrs.add(BotMessages.TextModernField(f.name,
131 | f.value, Option.apply(f.isShort)))
132 | }
133 | attachesList.add(BotMessages.TextModernAttach(m.title,
134 | m.titleUrl, null, m.text, convertParagraphStyle(m.paragraphStyle), attrs))
135 | }
136 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.TextMessage(message.compatText,
137 | Option.apply(BotMessages.TextModernMessage(message.text,
138 | null,
139 | null,
140 | paragraphStyle, attachesList))))
141 | }
142 |
143 | private fun convertParagraphStyle(style: ParagraphStyle?): BotMessages.ParagraphStyle? {
144 | if (style == null) {
145 | return null
146 | }
147 |
148 | return BotMessages.ParagraphStyle(Option.apply(style.showParagraph),
149 | convertColor(style.paragraphColor),
150 | convertColor(style.backgroundColor))
151 | }
152 |
153 | private fun convertColor(color: Color?): Option {
154 | if (color == null) {
155 | return Option.empty()
156 | }
157 | when (color) {
158 | Color.Green -> {
159 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Green$`()))
160 | }
161 | Color.Red -> {
162 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Red$`()))
163 | }
164 | Color.Yellow -> {
165 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Yellow$`()))
166 | }
167 | is Color.RGB -> {
168 | return Option.apply(BotMessages.RgbColor(color.number))
169 | }
170 | }
171 | }
172 |
173 | override fun findUser(query: String): BotMessages.User? {
174 | try {
175 | val res = Await.result(bot.requestFindUser(query), Duration.create(50, TimeUnit.SECONDS))
176 | if (res.users.isEmpty()) {
177 | return null
178 | }
179 | return res.users[0]
180 | } catch(e: Exception) {
181 | return null
182 | }
183 | }
184 |
185 | override fun getUser(uid: Int): BotMessages.User {
186 | return bot.getUser(uid)
187 | }
188 |
189 | override fun getGroup(gid: Int): BotMessages.Group {
190 | return bot.getGroup(gid)
191 | }
192 |
193 | override fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup? {
194 | try {
195 | return Await.result(bot.requestCreateGroup(groupTitle), Duration.create(50, TimeUnit.SECONDS))
196 | } catch(e: Exception) {
197 | return null
198 | }
199 | }
200 |
201 | override fun createHook(hookName: String): String? {
202 | try {
203 | return Await.result(bot.requestRegisterHook(hookName), Duration.create(50, TimeUnit.SECONDS)).value()
204 | } catch(e: Exception) {
205 | return null
206 | }
207 | }
208 |
209 | override fun createBot(userName: String, name: String): BotMessages.BotCreated? {
210 | try {
211 | return Await.result(bot.requestCreateBot(userName, name), Duration.create(50, TimeUnit.SECONDS))
212 | } catch(e: Exception) {
213 | return null
214 | }
215 | }
216 |
217 | override fun changeUserName(uid: Int, name: String): Boolean {
218 | try {
219 | Await.result(bot.requestChangeUserName(uid, name),
220 | Duration.create(50, TimeUnit.SECONDS))
221 | return true
222 | } catch(e: Exception) {
223 | e.printStackTrace()
224 | }
225 | return false
226 | }
227 |
228 | override fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean {
229 | try {
230 | Await.result(bot.requestChangeUserAvatar(uid,
231 | BotMessages.FileLocation(fileId, accessHash)),
232 | Duration.create(50, TimeUnit.SECONDS))
233 | return true
234 | } catch(e: Exception) {
235 | e.printStackTrace()
236 | }
237 | return false
238 | }
239 |
240 | override fun changeUserAbout(uid: Int, name: String): Boolean {
241 | try {
242 | Await.result(bot.requestChangeUserAbout(uid, Optional.of(name)),
243 | Duration.create(50, TimeUnit.SECONDS))
244 | return true
245 | } catch(e: Exception) {
246 | e.printStackTrace()
247 | }
248 | return false
249 | }
250 |
251 | override fun userIsAdmin(uid: Int): Boolean {
252 | try {
253 | return Await.result(bot.requestIsAdmin(uid), Duration.create(50, TimeUnit.SECONDS)).getIsAdmin()
254 | } catch(e: Exception) {
255 | e.printStackTrace()
256 | }
257 | return false
258 | }
259 |
260 | //TODO: remove magic numbers
261 | override fun createStickerPack(ownerUserId: Int): Int {
262 | try {
263 | return Await.result(bot.requestCreateStickerPack(ownerUserId), Duration.create(50, TimeUnit.SECONDS)).value().toInt()
264 | } catch(e: Exception) {
265 | e.printStackTrace()
266 | }
267 | return -1
268 | }
269 |
270 | override fun addSticker(ownerUserId: Int, packId: Int, emoji: Optional, image128: ByteArray, w128: Int, h128: Int, image256: ByteArray, w256: Int, h256: Int, image512: ByteArray, w512: Int, h512: Int): Boolean {
271 | try {
272 | Await.result(bot.requestAddSticker(ownerUserId, packId, emoji, image128, w128, h128, image256, w256, h256, image512, w512, h512),
273 | Duration.create(50, TimeUnit.SECONDS))
274 | return true
275 | } catch(e: Exception) {
276 | e.printStackTrace()
277 | }
278 | return false
279 | }
280 |
281 | override fun showStickerPacks(ownerUserId: Int): List {
282 | try {
283 | return Await.result(bot.requestShowStickerPacks(ownerUserId), Duration.create(50, TimeUnit.SECONDS)).getIds()
284 | } catch(e: Exception) {
285 | e.printStackTrace()
286 | }
287 | return ArrayList()
288 | }
289 |
290 | override fun showStickers(ownerUserId: Int, packId: Int): List {
291 | try {
292 | return Await.result(bot.requestShowStickers(ownerUserId, packId), Duration.create(50, TimeUnit.SECONDS)).getIds()
293 | } catch(e: Exception) {
294 | e.printStackTrace()
295 | }
296 | return ArrayList()
297 | }
298 |
299 | override fun deleteSticker(ownerUserId: Int, packId: Int, stickerId: Int): Boolean {
300 | try {
301 | Await.result(bot.requestDeleteSticker(ownerUserId, packId, stickerId), Duration.create(50, TimeUnit.SECONDS))
302 | return true
303 | } catch(e: Exception) {
304 | e.printStackTrace()
305 | }
306 | return false
307 | }
308 |
309 | override fun makeStickerPackDefault(userId: Int, packId: Int): Boolean {
310 | try {
311 | Await.result(bot.requestMakeStickerPackDefault(userId, packId), Duration.create(50, TimeUnit.SECONDS))
312 | return true
313 | } catch(e: Exception) {
314 | e.printStackTrace()
315 | }
316 | return false
317 | }
318 |
319 | override fun unmakeStickerPackDefault(userId: Int, packId: Int): Boolean {
320 | try {
321 | Await.result(bot.requestUnmakeStickerPackDefault(userId, packId), Duration.create(50, TimeUnit.SECONDS))
322 | return true
323 | } catch(e: Exception) {
324 | e.printStackTrace()
325 | }
326 | return false
327 | }
328 |
329 | override fun getFile(fileId: Long, accessHash: Long): ByteArray {
330 | try {
331 | return Await.result(bot.requestDownloadFile(BotMessages.FileLocation(fileId, accessHash)), Duration.create(50, TimeUnit.SECONDS)).fileBytes()
332 | } catch(e: Exception) {
333 | e.printStackTrace()
334 | }
335 | return ByteArray(0)
336 | }
337 |
338 | }
339 |
340 | class APITraitScopedImpl(val peer: OutPeer, bot: RemoteBot) : APITraitImpl(bot), APITraitScoped {
341 | override fun sendText(text: String) {
342 | sendText(peer, text)
343 | }
344 |
345 | override fun sendJson(dataType: String, json: JSONObject) {
346 | sendJson(peer, dataType, json)
347 | }
348 |
349 | override fun sendModernText(message: ModernMessage) {
350 | sendModernText(peer, message)
351 | }
352 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/AdminTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import im.actor.bots.BotMessages
4 | import im.actor.bots.framework.MagicForkScope
5 | import java.util.*
6 |
7 | /**
8 | * Trait for enabling administration control of Bot. Enables admins nicknames and checking if user is
9 | * bots's admin
10 | */
11 | interface AdminTrait {
12 | var admins: MutableList
13 | fun isAdmin(user: BotMessages.User?): Boolean
14 | }
15 |
16 | interface AdminTraitScoped : AdminTrait {
17 | fun isAdminScope(): Boolean
18 | }
19 |
20 | open class AdminTraitImpl : AdminTrait {
21 |
22 | override var admins: MutableList = ArrayList()
23 |
24 | override fun isAdmin(user: BotMessages.User?): Boolean {
25 | if (user == null) {
26 | return false
27 | }
28 | if (user.username.isPresent) {
29 | return admins.contains(user.username.get())
30 | } else {
31 | return false
32 | }
33 | }
34 | }
35 |
36 | class AdminTraitScopedImpl(val scope: MagicForkScope) : AdminTraitImpl(), AdminTraitScoped {
37 |
38 | override fun isAdminScope(): Boolean {
39 |
40 | if (scope.peer.isPrivate) {
41 | try {
42 | val usr = scope.bot.getUser(scope.peer.id) ?: return false
43 | return isAdmin(usr)
44 | } catch(e: Exception) {
45 |
46 | }
47 | }
48 |
49 | return false
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/AiTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import org.json.JSONObject
4 | import java.util.*
5 |
6 | /**
7 | * API.AI Integration trait.
8 | * Before use set your Subscription Key and register agents
9 | */
10 | interface AiTrait {
11 | var aiSubscriptionKey: String?
12 | fun registerAiAgent(key: String, lang: String, token: String)
13 |
14 | fun aiQuery(query: String): AiResponse?
15 |
16 | fun aiQuery(query: String, closure: AiResponse.() -> Unit)
17 |
18 | fun aiQuery(agent: String, query: String): AiResponse?
19 |
20 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit)
21 | }
22 |
23 | class AiTraitImpl : AiTrait,
24 | HTTPTrait by HTTPTraitImpl() {
25 |
26 | private val ENDPOINT = "https://api.api.ai/v1/"
27 | private var defaultAgent: String? = null
28 | private val agents = HashMap()
29 | private val session = Random().nextLong()
30 | override var aiSubscriptionKey: String? = null
31 |
32 | override fun registerAiAgent(key: String, lang: String, token: String) {
33 | if (aiSubscriptionKey == null) {
34 | throw RuntimeException("set subscription key first")
35 | }
36 | if (agents.containsKey(key)) {
37 | throw RuntimeException("$key agent is already registered!")
38 | }
39 | agents.put(key, AiAgent(key, lang, token))
40 | if (defaultAgent == null) {
41 | defaultAgent = key
42 | }
43 | }
44 |
45 | override fun aiQuery(query: String): AiResponse? {
46 | if (defaultAgent == null) {
47 | throw RuntimeException("No Agents registered!")
48 | }
49 | return aiQuery(defaultAgent!!, query)
50 | }
51 |
52 | override fun aiQuery(query: String, closure: AiResponse.() -> Unit) {
53 | aiQuery(query)?.closure()
54 | }
55 |
56 | override fun aiQuery(agent: String, query: String): AiResponse? {
57 | val agentConfig = agents[agent]!!
58 | val req = JSONObject()
59 | req.put("lang", agentConfig.lang)
60 | req.put("query", query)
61 | req.put("sessionId", "$session")
62 | val resp = aiRequest("query", agentConfig, req)
63 | if (resp != null) {
64 | return AiResponse(resp)
65 | } else {
66 | return null
67 | }
68 | }
69 |
70 | override fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit) {
71 | aiQuery(agent, query)?.closure()
72 | }
73 |
74 | private fun aiRequest(command: String, agent: AiAgent, request: JSONObject): JSONObject? {
75 | return (urlPostJson(ENDPOINT + command + "?v=20150910",
76 | Json.JsonObject(request), "Authorization", "Bearer ${agent.token}",
77 | "ocp-apim-subscription-key", aiSubscriptionKey!!) as? Json.JsonObject)?.json
78 | }
79 | }
80 |
81 | private data class AiAgent(val key: String, val lang: String, val token: String)
82 |
83 | class AiResponse(val raw: JSONObject) {
84 |
85 | val action: String
86 | val speech: String?
87 |
88 | val pQuery: String?
89 | val pSimplified: String?
90 | val pRequestType: String?
91 | val pSummary: String?
92 | val pTime: Date?
93 |
94 | init {
95 | val result = raw.getJSONObject("result")
96 | action = result.optString("action", "input.unknown")
97 |
98 | if (result.has("fulfillment")) {
99 | val fulfillment = result.getJSONObject("fulfillment")
100 | val speech2 = fulfillment.optString("speech")
101 | if (speech2 == "") {
102 | speech = null
103 | } else {
104 | speech = speech2
105 | }
106 | } else {
107 | speech = null
108 | }
109 | if (result.has("parameters")) {
110 | val p = result.getJSONObject("parameters")
111 | pQuery = p.optString("q")
112 | pSimplified = p.optString("simplified")
113 | pRequestType = p.optString("request_type")
114 | val pSummaryS = p.optString("summary")
115 | if (pSummaryS == "") {
116 | pSummary = null
117 | } else {
118 | pSummary = pSummaryS;
119 | }
120 | } else {
121 | pQuery = null
122 | pSimplified = null
123 | pRequestType = null
124 | pSummary = null
125 | }
126 |
127 | pTime = null;
128 | }
129 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/BugSnagtrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | var sharedBugSnagClient: com.bugsnag.Client? = null
4 |
5 | interface BugSnag {
6 | fun logException(e: Throwable?)
7 | }
8 |
9 | class BugSnagImpl() : BugSnag {
10 |
11 | override fun logException(e: Throwable?) {
12 | if (e != null) {
13 | sharedBugSnagClient?.notify(e)
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/DispatcherTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import akka.actor.Actor
4 | import akka.actor.Cancellable
5 | import scala.concurrent.duration.Duration
6 | import java.util.concurrent.TimeUnit
7 |
8 |
9 | interface DispatchTrait {
10 | fun initDispatch(actor: Actor)
11 | fun schedule(message: Any, delay: Long): Cancellable
12 | }
13 |
14 | class DispatchTraitImpl : DispatchTrait {
15 |
16 | private var actor: Actor? = null
17 |
18 | override fun schedule(message: Any, delay: Long): Cancellable {
19 | return schedullerSchedule(message, delay)
20 | }
21 |
22 | override fun initDispatch(actor: Actor) {
23 | this.actor = actor
24 | }
25 |
26 | private fun schedullerSchedule(message: Any, delay: Long): Cancellable {
27 | return actor!!.context().system().scheduler().scheduleOnce(Duration.create(delay, TimeUnit.MILLISECONDS), {
28 | actor!!.self().tell(message, actor!!.self())
29 | }, actor!!.context().dispatcher())
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/HTTPTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import org.apache.commons.io.IOUtils
4 | import org.apache.http.client.methods.HttpGet
5 | import org.apache.http.client.methods.HttpPost
6 | import org.apache.http.entity.ContentType
7 | import org.apache.http.entity.StringEntity
8 | import org.apache.http.impl.client.CloseableHttpClient
9 | import org.apache.http.impl.client.HttpClients
10 | import org.json.JSONArray
11 | import org.json.JSONObject
12 | import java.io.IOException
13 |
14 | /**
15 | * HTTP Requesting Trait
16 | */
17 | interface HTTPTrait {
18 | fun urlGetText(url: String, vararg headers: String): String?
19 | fun urlPostUrlEncodedText(url: String, content: String, vararg headers: String): String?
20 | fun urlPostJson(url: String, content: Json, vararg headers: String): Json?
21 | fun urlGetJson(url: String, vararg headers: String): Json?
22 | }
23 |
24 | class HTTPTraitImpl : HTTPTrait {
25 |
26 | private var client: CloseableHttpClient? = null
27 |
28 | override fun urlGetText(url: String, vararg headers: String): String? {
29 | assumeHttp()
30 | try {
31 | val get = HttpGet(url)
32 | for (i in 0..headers.size / 2 - 1) {
33 | get.addHeader(headers[i * 2], headers[i * 2 + 1])
34 | }
35 | val res = client!!.execute(get)
36 | val text = IOUtils.toString(res.entity.content)
37 | res.close()
38 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) {
39 | return text
40 | }
41 | return null
42 | } catch (e: IOException) {
43 | e.printStackTrace()
44 | }
45 |
46 | return null
47 | }
48 |
49 | override fun urlPostUrlEncodedText(url: String, content: String, vararg headers: String): String? {
50 | assumeHttp()
51 | try {
52 | val post = HttpPost(url)
53 | for (i in 0..headers.size / 2 - 1) {
54 | post.addHeader(headers[i * 2], headers[i * 2 + 1])
55 | }
56 | post.entity = StringEntity(content, ContentType.APPLICATION_FORM_URLENCODED)
57 | val res = client!!.execute(post)
58 | val text = IOUtils.toString(res.entity.content)
59 | res.close()
60 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) {
61 | return text
62 | }
63 | return null
64 | } catch (e: IOException) {
65 | e.printStackTrace()
66 | }
67 |
68 | return null
69 | }
70 |
71 | override fun urlPostJson(url: String, content: Json, vararg headers: String): Json? {
72 | assumeHttp()
73 | try {
74 | val post = HttpPost(url)
75 | for (i in 0..headers.size / 2 - 1) {
76 | post.addHeader(headers[i * 2], headers[i * 2 + 1])
77 | }
78 | post.entity = StringEntity(content.toString(), ContentType.APPLICATION_JSON)
79 | val res = client!!.execute(post)
80 | val text = IOUtils.toString(res.entity.content)
81 | res.close()
82 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) {
83 | return parseJson(text)
84 | }
85 | return null
86 | } catch (e: IOException) {
87 | e.printStackTrace()
88 | }
89 |
90 | return null
91 | }
92 |
93 | override fun urlGetJson(url: String, vararg headers: String): Json? {
94 | val res = urlGetText(url, *headers) ?: return null
95 | try {
96 | return parseJson(res)
97 | } catch (e: Exception) {
98 | e.printStackTrace()
99 | }
100 |
101 | return null
102 | }
103 |
104 | private fun assumeHttp() {
105 | if (client == null) {
106 | client = HttpClients.createDefault()
107 | }
108 | }
109 | }
110 |
111 | fun parseJson(text: String): Json? {
112 | try {
113 | return Json.JsonObject(JSONObject(text))
114 | } catch(e: Exception) {
115 |
116 | }
117 | try {
118 | return Json.JsonArray(JSONArray(text))
119 | } catch(e: Exception) {
120 |
121 | }
122 |
123 | return null
124 | }
125 |
126 | sealed class Json {
127 | class JsonObject(val json: JSONObject) : Json() {
128 | override fun toString(): String {
129 | return json.toString()
130 | }
131 | }
132 |
133 | class JsonArray(val json: JSONArray) : Json() {
134 | override fun toString(): String {
135 | return json.toString()
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/I18NTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import im.actor.bots.framework.i18n.I18NEngine
4 |
5 | interface I18NTrait {
6 | var language: String
7 | fun pickLocale(supported: Array, usersLocales: Array): String
8 | fun initLocalize(fileName: String)
9 | fun localized(key: String): String
10 | }
11 |
12 | fun I18NTrait.initLocalize(name: String, supported: Array, usersLocales: Array) {
13 | val locale = pickLocale(supported, usersLocales)
14 | if (locale == "en") {
15 | language = "en"
16 | initLocalize("$name.properties")
17 | } else {
18 | language = locale
19 | initLocalize("${name}_${locale.capitalize()}.properties")
20 | }
21 | }
22 |
23 | class I18NTraitImpl : I18NTrait {
24 |
25 | override var language: String = "en"
26 |
27 | private var i18n: I18NEngine? = null
28 |
29 | override fun pickLocale(supported: Array, usersLocales: Array): String {
30 | for (ul in usersLocales) {
31 |
32 | // TODO: Implement country checking
33 | val uLang = ul.substring(0, 2)
34 | for (s in supported) {
35 | if (s.substring(0, 2).toLowerCase() == uLang) {
36 | return s.toLowerCase()
37 | }
38 | }
39 | }
40 | return supported[0]
41 | }
42 |
43 | override fun initLocalize(fileName: String) {
44 | i18n = I18NEngine(fileName)
45 | }
46 |
47 | override fun localized(key: String): String {
48 | return i18n!!.pick(key)
49 | }
50 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/LogTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import akka.actor.Actor
4 | import akka.event.DiagnosticLoggingAdapter
5 | import akka.event.Logging
6 |
7 | /**
8 | * Logging Trait
9 | */
10 | interface LogTrait {
11 | fun initLog(root: Actor)
12 | fun d(msg: String)
13 | fun v(msg: String)
14 | }
15 |
16 | class LogTraitImpl() : LogTrait {
17 |
18 | private var LOG: DiagnosticLoggingAdapter? = null
19 |
20 | override fun initLog(root: Actor) {
21 | LOG = Logging.apply(root)
22 | }
23 |
24 |
25 | override fun v(msg: String) {
26 | LOG!!.info(msg)
27 | }
28 |
29 | override fun d(msg: String) {
30 | LOG!!.debug(msg)
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/ParseTrait.kt:
--------------------------------------------------------------------------------
1 | //package im.actor.bots.framework.traits
2 | //
3 | //import org.json.JSONObject
4 | //import java.net.URLEncoder
5 | //
6 | ///**
7 | // * Parse.com trait. Useful for storing bots's data in casual way. But, before use, try our built-in
8 | // * storage
9 | // */
10 | //interface ParseTrait {
11 | // fun parseAddObject(className: String, obj: JSONObject): String?
12 | // fun parseUpdateObject(className: String, id: String, obj: JSONObject): Boolean
13 | // fun parseGetObject(className: String, id: String): JSONObject?
14 | // fun parseFindObject(className: String, query: String): JSONObject?
15 | //}
16 | //
17 | //class ParseTraitImpl(val appId: String, val restApiKey: String) : ParseTrait,
18 | // HTTPTrait by HTTPTraitImpl() {
19 | //
20 | // private val ENDPOINT = "https://api.parse.com/1"
21 | //
22 | // override fun parseAddObject(className: String, obj: JSONObject): String? {
23 | // val res = urlPostJson("$ENDPOINT/classes/$className", obj,
24 | // "X-Parse-Application-Id", appId,
25 | // "X-Parse-REST-API-Key", restApiKey)
26 | // if (res != null) {
27 | // return res.getString("objectId")
28 | // }
29 | // return null
30 | // }
31 | //
32 | // override fun parseUpdateObject(className: String, id: String, obj: JSONObject): Boolean {
33 | // val res = urlPostJson("$ENDPOINT/classes/$className/$id", obj,
34 | // "X-Parse-Application-Id", appId,
35 | // "X-Parse-REST-API-Key", restApiKey)
36 | // return res != null
37 | // }
38 | //
39 | // override fun parseGetObject(className: String, id: String): JSONObject? {
40 | // throw NotImplementedError()
41 | // }
42 | //
43 | // override fun parseFindObject(className: String, query: String): JSONObject? {
44 | // val res = urlGetJson("$ENDPOINT/classes/$className?where=${URLEncoder.encode(query)}",
45 | // "X-Parse-Application-Id", appId,
46 | // "X-Parse-REST-API-Key", restApiKey)
47 | // if (res != null) {
48 | // val res = res.optJSONArray("results")
49 | // if (res != null) {
50 | // if (res.length() != 1) {
51 | // return null
52 | // } else {
53 | // return res.getJSONObject(0)
54 | // }
55 | // }
56 | // }
57 | // return null
58 | // }
59 | //}
--------------------------------------------------------------------------------
/actor-bots/src/main/java/im/actor/bots/framework/traits/SMTPTrait.kt:
--------------------------------------------------------------------------------
1 | package im.actor.bots.framework.traits
2 |
3 | import com.google.protobuf.StructOrBuilder
4 | import org.codemonkey.simplejavamail.Email
5 | import org.codemonkey.simplejavamail.Mailer
6 | import org.codemonkey.simplejavamail.TransportStrategy
7 | import java.util.*
8 | import javax.mail.*
9 |
10 | interface SMTPTrait {
11 | fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String,
12 | to: List, cc: List = ArrayList(),
13 | bcc: List = ArrayList())
14 |
15 | fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String,
16 | to: String)
17 |
18 | fun sendEmail(subject: String, text: String, from: String, fromEmail: String, to: String)
19 | }
20 |
21 | class SMTPTraitIml(val login: String, val password: String,
22 | val smtpHost: String, val smtpPort: Int) : SMTPTrait {
23 |
24 | private var mailer: Mailer? = null
25 |
26 | override fun sendEmail(subject: String, text: String, from: String, fromEmail: String, to: String) {
27 | sendEmail(subject, text, text, from, fromEmail, to)
28 | }
29 |
30 | override fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String, to: String) {
31 | var to2 = ArrayList()
32 | to2.add(to)
33 | sendEmail(subject, text, html, from, fromEmail, to2)
34 | }
35 |
36 | override fun sendEmail(subject: String,
37 | text: String, html: String,
38 | from: String, fromEmail: String,
39 | to: List,
40 | cc: List,
41 | bcc: List) {
42 |
43 | val email = Email()
44 | email.setFromAddress(from, fromEmail)
45 | email.subject = subject
46 | email.text = text
47 | email.textHTML = html
48 | for (i in to) {
49 | email.addRecipient(i, i, Message.RecipientType.TO)
50 | }
51 | for (i in cc) {
52 | email.addRecipient(i, i, Message.RecipientType.CC)
53 | }
54 | for (i in bcc) {
55 | email.addRecipient(i, i, Message.RecipientType.BCC)
56 | }
57 |
58 | if (mailer == null) {
59 | mailer = Mailer(smtpHost, smtpPort, login, password, TransportStrategy.SMTP_TLS)
60 | }
61 | mailer!!.sendMail(email)
62 | }
63 | }
--------------------------------------------------------------------------------
/actor-bots/src/main/resources/BotFather.properties:
--------------------------------------------------------------------------------
1 | message.start=Hi! I can help you create and manage your Actor bots. \
2 | Please, read [manual](https://actor.readme.io/docs/bots-getting-started) before we begin.\n \
3 | Feel free to ask any questions about bots in our OSS Group.\n\n \
4 | You can control me by sending these commands\n \
5 | [/newbot](send:/newbot) - creating new bot\n \
6 | [/setphoto](send:/setphoto) - setting bot's avatar\n \
7 | [/setname](send:/setname) - setting bot's visible name\n \
8 | [/setabout](send:/setabout) - setting bot description\n \
9 | [/list](send:/list) - list your bots\n \
10 | [/cancel](send:/cancel) - Cancel current operation
11 |
12 | message.new=New bot? Alright. How do we name it? Please, choose a name for your bot bot.
13 | message.new_invalid=Please, send me bot name or *[/cancel](send:/cancel)* for cancel bot creation.
14 |
15 | message.new_username=Good. Now let's choose a username for your bot.
16 | message.new_username_used=Unable to create bot with this username, please, try again with another one.
17 | message.new_username_short=Sorry, but bot username might be at least 5 letters
18 | message.new_username_long=Sorry, but bot username can't be longer than 32 letters
19 | message.new_username_invalid=Please, send me valid bot username.
20 |
21 | message.new_success=Success! Bot's token is $token. Now you can set photo by sending me [/setphoto](send:/setphoto) command
22 |
23 | message.list_empty=You don't have any available bot. You can create new bot with [/newbot](send:/newbot) command.
24 | message.list=Your bots:
25 |
26 | message.edit_name=Please, send me bot's username for name change.
27 | message.edit_name_ask=Ok, now Send me new name for bot.
28 | message.edit_name_ask_invalid=Please, send me valid name!
29 | message.edit_name_error=Unable to change name. Exiting. Please, try again later.
30 |
31 | message.edit_photo=Please, send me bot's username for photo change.
32 | message.edit_photo_ask=Ok, now Send me new photo for bot.
33 | message.edit_photo_ask_invalid=Please, send me valid photo!
34 | message.edit_photo_error=Unable to change photo. Exiting. Please, try again later.
35 |
36 | message.edit_about=Please, send me bot's username for about change.
37 | message.edit_about_ask=Ok, now Send me new about for bot.
38 | message.edit_about_ask_invalid=Please, send me valid about!
39 | message.edit_about_error=Unable to change about. Exiting. Please, try again later.
40 |
41 | message.pick_not_your=This is not your bot!
42 | message.pick_human=Hey! This is not a bot at all!
43 | message.pick_nothing_found=Unable to find bot with username @$text. Please, try again or send [/cancel](send:/cancel) to stop.
44 |
45 | message.success=Success! [Anything else?](send:/start)
46 | message.cancel=Ok. [Anything else?](send:/start)
47 |
48 | message.unknown.0=Command is invalid. Say what?
49 | message.unknown.1=Command is invalid. I really didn't get it...
50 | message.unknown.2=Command is invalid. What do you mean?
51 | message.unknown.3=Command is invalid. Please, say it again in a good way.
--------------------------------------------------------------------------------
/actor-bots/src/main/resources/BotFather_Ru.properties:
--------------------------------------------------------------------------------
1 | message.start=Привет! Я здесь что бы помочь тебе создать твоих Ботов в Акторе. \
2 | Пожалуйста, прочитай [документацию](https://actor.readme.io/docs/bots-getting-started) прежде чем начать.\n \
3 | И не стесняйся задавать вопросы в группе поддержки OpenSource.\n\n \
4 | Ты можешь отправлять мне следующие комманды\n \
5 | [/newbot](send:/newbot) - Создать нового бота\n \
6 | [/setphoto](send:/setphoto) - Установка изображения бота\n \
7 | [/setname](send:/setname) - Поменять имя бота\n \
8 | [/setabout](send:/setabout) - Поменять описание бота\n \
9 | [/list](send:/list) - Показать список своих ботов
10 |
11 | message.new=Новый бот? Замечательно. Как мы назовем его? Пожалуйста, отправь мне его имя.
12 | message.new_invalid=Пожалуйста, пришли мне имя или [/cancel](send:/cancel) для отмены
13 |
14 | message.new_username=Хорошо. Теперь необходимо выбрать ник для бота.
15 | message.new_username_used=Этот ник уже используется, пожалуйста, попробуй другой.
16 | message.new_username_short=К сожалению, ник должен быть как минимум 5 символов длинной
17 | message.new_username_long=К сожалению, ник не может быть больше 32 символов
18 | message.new_username_invalid=Пожалуйста, пришли мне ник бота.
19 |
20 | message.new_success=Готово! Токен доступа нового бота - $token. Теперь ты можешь установить фото бота, отправив команду [/setphoto](send:/setphoto).
21 |
22 | message.list_empty=У Вас нет зарегистрированных ботов. Создать бота можно командой [/newbot](send:/newbot).
23 | message.list=Ваши боты:
24 |
25 | message.edit_name=Пожалуйста, отправь ник бота.
26 | message.edit_name_ask=Хорошо, теперь новое имя для него.
27 | message.edit_name_ask_invalid=Пожалуйста, пришли мне имя!
28 | message.edit_name_error=Невозможно поменять имя. Пожалуйста, попробуйте позже.
29 |
30 | message.edit_photo=Пожалуйста, отправь ник бота.
31 | message.edit_photo_ask=Хорошо, теперь новое фото для него.
32 | message.edit_photo_ask_invalid=Пожалуйста, пришли мне фото!
33 | message.edit_photo_error=Невозможно поменять фото бота. Пожалуйста, попробуйте позже.
34 |
35 | message.edit_about=Пожалуйста, отправь ник бота.
36 | message.edit_about_ask=Хорошо, теперь новое описание для него.
37 | message.edit_about_ask_invalid=Пожалуйста, пришли мне описание!
38 | message.edit_about_error=Невозможно поменять описание. Пожалуйста, попробуйте позже.
39 |
40 | message.pick_not_your=Это не Ваш бот!
41 | message.pick_human=Хей! Это вообще не бот!
42 | message.pick_nothing_found=Не могу найти бота с ником @$text. Попробуйте еще или отправьте [/cancel](send:/cancel) что бы отменить операцию.
43 |
44 | message.success=Готово! [Что-нибудь еще?](send:/start)
45 | message.cancel=Отменено. [Что-нибудь еще?](send:/start)
46 |
47 | message.unknown.0=Неизвестная команда. Повторите?
48 | message.unknown.1=Неизвестная команда. Я правда не понимаю...
49 | message.unknown.2=Неизвестная команда. Что вы имели ввиду?
50 | message.unknown.3=Неизвестная команда. Пожалуйста, попробуйте вежливее.
--------------------------------------------------------------------------------
/actor-bots/src/main/resources/BotFather_Zn.properties:
--------------------------------------------------------------------------------
1 | message.start=您好!我可以帮您创建和管理您的优聆机器人。 我们开始之前,请阅读[手册](https\://actor.readme.io/docs/bots-getting-started)。\n关于优聆机器人的任何问题,欢迎您在我们的开源群中提问。\n\n 您可以通过如下命令控制我:\n [/newbot](send\:/newbot) - 创建新的机器人\n [/setphoto](send\:/setphoto) - 设置机器人的头像\n [/setname](send\:/setname) - 设置机器人的显示名称\n [/setabout](send\:/setabout) - 设置机器人的简介\n [/list](send\:/list) - 列出您的所有机器人\n [/cancel](send\:/cancel) - 取消当前操作
2 |
3 | message.new=新机器人? 好吧,我们怎么命名它呢? 请为您的机器人起个名字。
4 | message.new_invalid=请发机器人的名字给我,或者发送 *[/cancel](send\:/cancel)*来取消机器人的创建。
5 |
6 | message.new_username=好。现在让我们为您的机器人选择昵称。
7 | message.new_username_used=无法使用此昵称创建机器人,请使用其他昵称重试。
8 | message.new_username_short=抱歉,机器人的昵称至少要5个字符。
9 | message.new_username_long=抱歉, 机器人的昵称不能长于32个字符。
10 | message.new_username_invalid=请发给我合法的机器人昵称。
11 |
12 | message.new_success=成功\! 机器人的令牌为: $token。现在您可以向我发送[/setphoto](send\:/setphoto) 命令设置头像。
13 |
14 | message.list_empty=您没有任何可用机器人。您可以使用[/newbot](send\:/newbot)命令创建机器人。
15 | message.list=您的机器人\:
16 |
17 | message.edit_name=为了重命名请发送机器人的昵称给我。
18 | message.edit_name_ask=好的,现在发送机器人的新名字给我。
19 | message.edit_name_ask_invalid=请发送合法的名字给我!
20 | message.edit_name_error=无法更改名称。退出。请稍候再试。
21 |
22 | message.edit_photo=为了更换头像,请发送机器人的昵称给我。
23 | message.edit_photo_ask=好的,现在发送机器人的新头像给我。
24 | message.edit_photo_ask_invalid=请发送合法的头像给我。
25 | message.edit_photo_error=无法更换头像。退出。请稍候再试。
26 |
27 | message.edit_about=为了更新简介,请发送机器人的昵称给我。
28 | message.edit_about_ask=好的,现在发送机器人的新简介给我。
29 | message.edit_about_ask_invalid=请发送合法的简介给我。
30 | message.edit_about_error=无法更新简介。退出。请稍候再试。
31 |
32 | message.pick_not_your=这不是你的机器人!
33 | message.pick_human=您好!这根本不是机器人!
34 | message.pick_nothing_found=无法通过昵称@$text找到机器人。请重试或发送[/cancel](send\:/cancel)取消。
35 |
36 | message.success=成功\! [其他操作?](send\:/start)
37 | message.cancel=好的\! [其他操作?](send\:/start)
38 |
39 | message.unknown.0=命令不合法。您说什么?
40 | message.unknown.1=命令不合法。我真的不明白?
41 | message.unknown.2=命令不合法。您什么意思?
42 | message.unknown.3=命令不合法。请您再好好说一下。
43 |
--------------------------------------------------------------------------------
/actor-bots/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/actorapp/actor-bots/ea6c028726b236369b92381462baae752754aedb/build.gradle
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Recomendation
2 | We recommend you to read all guides one by one until you finish [Stateful Bot](tutorials/bot-stateful.md), the most important one.
3 |
4 | ## Guides
5 | * [About Bots](tutorials/bot-about.md)
6 | * [Registering new bot](tutorials/bot-register.md)
7 | * [Implementing bot](tutorials/bot-implement.md)
8 | * [Running bots](tutorials/bot-farm.md)
9 | * [Persistent Bot](tutorials/bot-persistent.md)
10 | * [Stateful Bot](tutorials/bot-stateful.md)
11 | * [Implementing Overlord](tutorials/bot-overlord.md)
12 | * [Incoming WebHooks](tutorials/web-hooks.md)
13 | * [Message Types](tutorials/bot-messages.md)
14 |
15 | ## Modules
16 | * [Bot API Module](api/API.md)
17 | * [Server key-value storage](api/key-value-server.md)
18 | * [Local key-value storage](api/key-value-local.md)
19 | * [Natural language processing with api.ai](api/ai.md)
20 | * [HTTP helpers](api/HTTP.md)
21 | * [i18n support](api/I18N.md)
22 | * [Helpers for admin users of bot](api/admin.md)
23 |
24 | ## Examples
25 | * [BotFather Implementation](../actor-bots/src/main/java/im/actor/bots/embedded/BotFather.kt)
26 | * [Notification bot with Overlord](../actor-bots/src/main/java/im/actor/bots/blocks/Notification.kt)
27 |
--------------------------------------------------------------------------------
/docs/api/API.md:
--------------------------------------------------------------------------------
1 | # API Module
2 |
3 | This module provide access to Actor Bot API.
4 |
5 | ```kotlin
6 | interface APITrait {
7 |
8 | //
9 | // Messaging
10 | //
11 |
12 | fun sendText(peer: OutPeer, text: String)
13 |
14 | fun findUser(query: String): BotMessages.User?
15 |
16 | fun getUser(uid: Int): BotMessages.User
17 |
18 | fun getGroup(gid: Int): BotMessages.Group
19 |
20 | fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup?
21 |
22 | fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean
23 |
24 | //
25 | // Managing Hooks
26 | //
27 |
28 | fun createHook(hookName: String): String?
29 |
30 | //
31 | // Super Bot methods
32 | //
33 |
34 | fun createBot(userName: String, name: String): BotMessages.BotCreated?
35 | fun changeUserName(uid: Int, name: String): Boolean
36 | fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean
37 | fun changeUserAbout(uid: Int, name: String): Boolean
38 | }
39 |
40 | interface APITraitScoped : APITrait {
41 | fun sendText(text: String)
42 | }
43 | ```
44 |
45 | ## Messaging API
46 |
47 | #### Sending Text
48 |
49 | ```fun sendText(peer: OutPeer, text: String)```
50 |
51 | #### Finding user (or bot) by phone number, email or nickname.
52 |
53 | ```fun findUser(query: String): BotMessages.User?```
54 |
55 | #### Getting cached User object by uid.
56 | NOTE: Cache doesn't saved on disk and lost on actor restart.
57 |
58 | ```fun getUser(uid: Int): BotMessages.User```
59 |
60 | #### Getting cached Group object by gid.
61 | NOTE: Cache doesn't saved on disk and lost on actor restart.
62 |
63 | ```fun getGroup(gid: Int): BotMessages.Group```
64 |
65 | ### Creating New Group
66 | ```fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup?```
67 |
68 | #### Inviting People to Group
69 | ```fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean```
70 |
71 |
72 |
73 | ## WebHooks API
74 | Useful for receiving messages in bot from external services
75 |
76 | #### Create New Hook
77 | Returns null if unable to create hook or it is already exists.
78 |
79 | ```fun createHook(hookName: String): String?```
80 |
81 |
82 |
83 | ## Super Bot API
84 | This API is only for bots that have admin privilegies.
85 |
86 | #### Create new bot
87 |
88 | ```fun createBot(userName: String, name: String): BotMessages.BotCreated?```
89 |
90 | #### Change User's name
91 |
92 | ```fun changeUserName(uid: Int, name: String): Boolean```
93 |
94 | #### Change User's avatar
95 |
96 | ```fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean```
97 |
98 | #### Change User's about
99 |
100 | ```fun changeUserAbout(uid: Int, name: String): Boolean```
101 |
--------------------------------------------------------------------------------
/docs/api/HTTP.md:
--------------------------------------------------------------------------------
1 | # HTTP Module
2 |
3 | This module contains helpers for making HTTP Requests and JSON-HTTP.
4 |
5 | ```kotlin
6 | interface HTTPTrait {
7 | fun urlGetText(url: String, vararg headers: String): String?
8 | fun urlPostJson(url: String, content: JSONObject, vararg headers: String): JSONObject?
9 | fun urlGetJson(url: String, vararg headers: String): JSONObject?
10 | fun urlGetJsonArray(url: String, vararg headers: String): JSONArray?
11 | }
12 | ```
13 |
14 | ## Usage
15 |
16 | For GET request, call `fun urlGetText(url: String, vararg headers: String): String?`. Result is not-null if request was successfull and null if not. Headers are array of keys and values.
17 |
18 | For JSON POST request, call `fun urlPostJson(url: String, content: JSONObject, vararg headers: String): JSONObject?` and this method witll post content to `url` with json content type and read response to json object
19 |
20 | For JSON GET requests, call `fun urlGetJson(url: String, vararg headers: String): JSONObject?` or `fun urlGetJsonArray(url: String, vararg headers: String): JSONArray?`. First one if you expect json object and second one if json array.
21 |
--------------------------------------------------------------------------------
/docs/api/I18N.md:
--------------------------------------------------------------------------------
1 | # I18N Module
2 |
3 | When writing bot you usually need texts for messages and it is not cool to store them in your code. Also when you want to make your bot global, you usually need to localize your app and this module helps you with this too.
4 |
5 | ```kotlin
6 | interface I18NTrait {
7 | var language: String
8 | fun pickLocale(supported: Array, usersLocales: Array): String
9 | fun initLocalize(fileName: String)
10 | fun initLocalize(name: String, supported: Array, usersLocales: Array)
11 | fun localized(key: String): String
12 | }
13 | ```
14 |
15 | # Language Files
16 | Language files are simple java .properties files, stored in resoruces directory of your project.
17 |
18 | If you want to show one random text from finite number of text you can write them with random integer in the end and I18N library will rotate them for you
19 |
20 | Example for `message.hello` string:
21 | ```
22 | message.hello.0=Hello!
23 | message.hello.1=Hi!
24 | message.hello.2=Alloha!
25 | ```
26 |
27 | # Initialization
28 |
29 | I18N module can run in two modes: localized and plain text. First one is localized version for each user and second one is just useful text source.
30 |
31 | Localization support is enabled by calling `fun initLocalize(name: String, supported: Array, usersLocales: Array)`, where ```name``` is base name of language files, ```supported``` is list of supported languages(lowcase) and ```userLocales``` - destination user locales.
32 |
33 | Language files are simple java .properties files, stored in resoruces path of your project. You can see example in bot father [strings](https://github.com/actorapp/actor-bots/tree/master/actor-bots/src/main/resources). Naming convention for language files is `_.properties` for translations and `.properties` for englis.
34 |
35 | If you don't want to have localization support, just run ```fun initLocalize(fileName: String)``` with exact file name (with .properties extension).
36 |
37 | # Usage
38 |
39 | Jsut call ```fun localized(key: String): String``` to get string. If string is not present exception is thrown.
40 |
41 | In some cases it might be useful to know what locale was selected during init, then you can use `language` variable for this.
42 |
--------------------------------------------------------------------------------
/docs/api/admin.md:
--------------------------------------------------------------------------------
1 | # Admin
2 |
3 | This module provide you simple helper for checking if some person is admin of this bot. All admins are hardcoded during bot initialization.
4 |
5 | ## Usage
6 |
7 | In initialization method of your bot register all admins:
8 |
9 | ```kotlin
10 | admins.add("steve")
11 | admins.add("prettynatty")
12 | ```
13 |
14 | After this you can check if user is admin:
15 |
16 | ```kotlin
17 | isAdmin()
18 | ```
19 |
--------------------------------------------------------------------------------
/docs/api/ai.md:
--------------------------------------------------------------------------------
1 | # Artifical Intelligence
2 |
3 | This module provide you easy access to [api.ai](https://api.ai) to build your Siri-like bot.
4 |
5 | Before begin we recommend to read api.ai [documentation](https://docs.api.ai/) first and understand basic principes.
6 |
7 | ```kotlin
8 | interface AiTrait {
9 | var aiSubscriptionKey: String?
10 | fun registerAiAgent(key: String, lang: String, token: String)
11 | fun aiQuery(query: String): AiResponse?
12 | fun aiQuery(query: String, closure: AiResponse.() -> Unit)
13 | fun aiQuery(agent: String, query: String): AiResponse?
14 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit)
15 | }
16 | ```
17 |
18 | ## Query Response
19 |
20 | ```kotlin
21 | class AiResponse {
22 | val raw: JSONObject
23 | val action: String
24 | val speech: String?
25 |
26 | val pQuery: String?
27 | val pSimplified: String?
28 | val pRequestType: String?
29 | val pSummary: String?
30 | val pTime: Date?
31 | }
32 | ```
33 |
34 | `raw` - Raw Response from API.AI
35 | `action` - Recognized action name
36 | `speech` - Suggested text response
37 |
38 | `pQuery` - query parameter for some actions (like searching on the web)
39 | `pSimplified` - simplified input string. For example, "hi", "hello", "nice to meet you!" will be simplified to "hello"
40 | `pRequestType` - type of request (domain or agent)
41 | `pSummary` - summary of input string
42 | `pTime` - recognized time
43 |
44 | ## Configuration
45 |
46 | Before using ai methods you need to provide subscription key and register your agents.
47 | Module can work with multiple agents. First registered agent is considered as default.
48 |
49 | ```kotlin
50 | aiSubscriptionKey = ""
51 | registerAiAgent("", "", "")
52 | ```
53 |
54 | ## Queries
55 |
56 | ### Performing AI query
57 |
58 | ```kotlin
59 | fun aiQuery(query: String): AiResponse?
60 | fun aiQuery(agent: String, query: String): AiResponse?
61 | ```
62 |
63 | ### Query with closure
64 |
65 | ```kotlin
66 | fun aiQuery(query: String, closure: AiResponse.() -> Unit)
67 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit)
68 | ```
69 |
70 | Same as above, but provide nice syntax like:
71 |
72 | ```kotlin
73 | aqQuery(text) {
74 | when(action) {
75 | "smalltalk.greetings" -> {
76 | sendText("Hello!")
77 | }
78 | }
79 | }
80 | ```
81 |
--------------------------------------------------------------------------------
/docs/api/key-value-local.md:
--------------------------------------------------------------------------------
1 | # Local Key-Value storage
2 |
3 | For storing information on bot's machine, it is built-in key-value storage based on leveldb.
4 | API is a bit overwhelming and we suggest you to use server key-value instead.
5 |
6 | ## Create KeyValue
7 |
8 | ```kotlin
9 | val stateKeyValue: SimpleKeyValueJava = ShardakkaExtension.get(context().system()).simpleKeyValue("").asJava()
10 | ```
11 |
12 | ## Using KeyValue
13 |
14 | ```kotlin
15 |
16 | // Reading value
17 | val value = stateKeyValue.get("")
18 |
19 | // Writing value
20 | stateKeyValue.syncUpsert("", ") {
22 | farm("bots") {
23 | bot(EchoBot::class) {
24 | name = "echo"
25 | token = ""
26 | traceHook = ""
27 | overlord =
28 | }
29 | }
30 | }
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/tutorials/bot-implement.md:
--------------------------------------------------------------------------------
1 | # Implementing your first bot
2 |
3 | For fast start, download sources of this repository and import it to IntelliJ IDEA 15 CE. Open actor-bots-example project and add new kotlin file `MyEchoBot.kt`.
4 |
5 | For your first bot, you only need to subclass from MagicBotFork and implement onMessage method:
6 |
7 | ```kotlin
8 | import im.actor.bots.framework.*
9 |
10 | class MyEchoBot(scope: MagicForkScope) : MagicBotFork(scope) {
11 |
12 | override fun onMessage(message: MagicBotMessage) {
13 | when (message) {
14 | is MagicBotTextMessage -> {
15 | sendText("Received: ${message.text}")
16 | }
17 | }
18 | }
19 | }
20 | ```
21 |
22 | That's all!
23 |
24 | ## Next Step
25 |
26 | Now you can [run](bot-farm.md) your first bot.
27 |
--------------------------------------------------------------------------------
/docs/tutorials/bot-messages.md:
--------------------------------------------------------------------------------
1 | # Message Types
2 |
3 | Bot Platform have several message types: text, document (with photo, video, audio extensions), service messages and abstract JSON-message.
4 |
5 | ```kotlin
6 | public abstract class MagicBotMessage(val peer: BotMessages.OutPeer, val sender: BotMessages.UserOutPeer,
7 | val rid: Long) {
8 |
9 | }
10 |
11 | public class MagicBotTextMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long,
12 | val text: String) : MagicBotMessage(peer, sender, rid) {
13 | var command: String? = null
14 | var commandArgs: String? = null
15 | }
16 |
17 | public class MagicBotJsonMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long,
18 | val json: JSONObject) : MagicBotMessage(peer, sender, rid) {
19 |
20 | }
21 |
22 | public class MagicBotDocMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long,
23 | val doc: BotMessages.DocumentMessage) : MagicBotMessage(peer, sender, rid) {
24 |
25 | }
26 | ```
--------------------------------------------------------------------------------
/docs/tutorials/bot-overlord.md:
--------------------------------------------------------------------------------
1 | # Overlord
2 |
3 | Overlord is an Actor that is not binded to specific conversation. Currently overlords is responsible to receiving web hooks.
4 |
5 | ### Implementing
6 |
7 | For implementing overlord, you need to subclass MagicOverlord class and implement required methods:
8 | ```kotlin
9 | class ExampleOverlord(scope: MagicOverlordScope) : MagicOverlord(scope) {
10 | override fun onRawWebHookReceived(name: String, body: ByteArray, headers: JSONObject) {
11 | // Implement Hook Processing
12 | }
13 | }
14 | ```
15 |
16 | ### Registering
17 |
18 | Also you need to register overlord class:
19 | ```kotlin
20 | farm("BotFarm") {
21 | bot(EchoBot::class) {
22 | name = "echo"
23 | token = ""
24 | traceHook = ""
25 | overlord = ExampleOverlord::class
26 | }
27 | }
28 | ```
29 |
30 | ### Sending Message to Overlord
31 |
32 | This message can be received in `onReceive` method of overlord. WARRING: Do not forget to call super class method.
33 |
34 | ```kotlin
35 | fun sendToOverlord(object: Any)
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/tutorials/bot-persistent.md:
--------------------------------------------------------------------------------
1 | # Persist bot
2 |
3 | For much easier save/restore of bot's state there is `MagicPersistentBot` that saves it's state to disk after each incoming message.
4 |
5 | Everyting you need is only subclass from `MagicPersistentBot` and implement `onRestoreState` and `onSaveState`. On bot restart onRestoreState is called and you can easily restore saved state.
6 |
7 | ```kotlin
8 | import im.actor.bots.framework.*
9 | import im.actor.bots.framework.persistence.*
10 | import org.json.JSONObject
11 |
12 | class EchoPersistentBot(scope: MagicForkScope) : MagicPersistentBot(scope) {
13 |
14 | var receivedCount: Int = 0
15 |
16 | override fun onRestoreState(state: JSONObject) {
17 | receivedCount = state.optInt("counter", 0)
18 | }
19 |
20 | override fun onMessage(message: MagicBotMessage) {
21 | sendText("Received ${receivedCount++} messages")
22 | }
23 |
24 | override fun onSaveState(state: JSONObject) {
25 | state.put("counter", receivedCount)
26 | }
27 | }
28 | ```
--------------------------------------------------------------------------------
/docs/tutorials/bot-register.md:
--------------------------------------------------------------------------------
1 | # Register bot
2 |
3 | For registering bot, you need to find `@botfather` in Actor Cloud and send him `/start` command.
4 |
5 | After registering new bot, you will get **AUTH_TOKEN** for your bot instance.
6 |
7 | ## Next Step
8 |
9 | Now you can start to implement your [first bot](bot-implement.md).
10 |
--------------------------------------------------------------------------------
/docs/tutorials/bot-stateful.md:
--------------------------------------------------------------------------------
1 | # Stateful bot
2 |
3 | Bots are usually have some states. For example, waiting for user command or expecting user input of specific type doin some operation and so on. It is very hard to maintain such states and `MagicStatefulBot` comes to the aid.
4 |
5 | ## Defining States
6 |
7 | When you create subclass from `MagicStatefulBot`, you need to implement `fun configure()` method, where you should define all bots states. States are not dynamically created at the runtime, it's a static tree of states.
8 |
9 | Every state has a name, full form of which is formed by spliting all parents state names with dots. State can easily goto any other state through `goto`, `tryGoto` and `gotoParent` methods. State names passed to these methods can be either short or full. If you pass short state name, method state lookup in all descendants states and it's childs for appropriate state.
10 |
11 | Root state "main" is a state, that expects slash-commands from user. When you need to go to the root state, you can simply call `goto("main")`.
12 |
13 | ## Persistent
14 |
15 | Stateful Bot can persist its state automatically. You can enable this feature by calling `enablePersistent = false` at the beginning of your `configure` method.
16 |
17 | ## Example
18 |
19 | ```kotlin
20 | class ExampleStatefulBot(scope: MagicForkScope) : MagicStatefulBot(scope) {
21 |
22 | override fun configure() {
23 | oneShot("/help") {
24 | sendText("Hello! I am example bot!")
25 | }
26 | raw("/test") {
27 | before {
28 | sendText("Send me something")
29 | }
30 | received {
31 | if (isText) {
32 | sendText("Text received")
33 | } else if (isDoc) {
34 | sendText("Doc received")
35 | } else {
36 | sendText("Something other received")
37 | }
38 | goto("main")
39 | }
40 | }
41 | command("/input") {
42 | before {
43 | sendText("Please, send me text")
44 | goto("ask_text")
45 | }
46 |
47 | expectInput("ask_text") {
48 |
49 | received {
50 | sendText("Yahoo! $text received!")
51 | goto("main")
52 | }
53 |
54 | validate {
55 | if (!isText) {
56 | sendText("I need text!")
57 | return@validate false
58 | }
59 |
60 | return@validate true
61 | }
62 | }
63 | }
64 | }
65 | }
66 | ```
67 |
68 | ## State types
69 |
70 | ### Expect Commands
71 |
72 | Default initial bot state. ExpectCommands automatically parse commands and route it to particular child state. If there is no state for sent command or it is not a command, state tries to route to state with name "default".
73 |
74 | ```kotlin
75 | expectCommands {
76 | oneShot("/help") {
77 | sendText("Help!")
78 | }
79 | oneShot("/hint") {
80 | sendText("Hint!")
81 | }
82 | }
83 | ```
84 |
85 | ### One-Shot state
86 | This state initially executes its body and immediately goes to parent. Very useful for responses that doesn't require user input.
87 |
88 | ```kotlin
89 | oneShot("/hint") {
90 | sendText("Hello! I am example bot!")
91 | }
92 |
93 | oneShot("/weather") {
94 | val weather = getUrlJson("weather_service_url")
95 | sendText("Expected temperature ${weather.getString("temperature")}")
96 | }
97 | ```
98 |
99 | ### Input state
100 | State for expecting user's input. Before entering this state, `before` closure is called. `validate` closure is called within any new message, where you need to validate input text for correctness. If the values will return `true`, `received` closure will be called.
101 |
102 | ```kotlin
103 | expectInput("ask_name") {
104 | before {
105 | sendText("Please, enter your name")
106 | }
107 | received {
108 | sendText("Thank you, $text")
109 | }
110 | validate {
111 | if (!isText) {
112 | sendText("Please, send text")
113 | return@validate false
114 | }
115 | return@validate true
116 | }
117 | }
118 | ```
119 |
120 | ### Raw state
121 | This is state that doesn't have any specific behaviour. Before entering state `before` closure is executed and on any new message `receive` closure is executed.
122 |
123 | ```kotlin
124 | raw("/translate") {
125 | before {
126 | sendText("Send me messages for translation! and /cancel to stop.")
127 | }
128 | received {
129 | if (isText) {
130 | sendText("Translated ${translate(text)}")
131 | } else if (isCommand) {
132 | if (command == "cancel") {
133 | gotoParent()
134 | }
135 | }
136 | }
137 |
138 | }
139 | ```
140 |
141 | ### Command state
142 | Obsolete state that acts like `raw`, but marks state as state for some specific command.
143 |
--------------------------------------------------------------------------------
/docs/tutorials/web-hooks.md:
--------------------------------------------------------------------------------
1 | # WebHooks Module
2 |
3 | This module allows you to create web hooks for bots and receive information from extenal services. With web hooks you can not only receive data but also implement OAuth2 authentication in external services.
4 |
5 | ### Create New WebHook
6 |
7 | Creating new web hook is simple: you just need to call a method from API Module - ```createHook(name: String): String?``` and in result you will get web hook url.
8 |
9 | ### Receve WebHook
10 |
11 | Receiving web hooks must be implemented in [Overlord](Overlord.md).
12 |
13 | ### Send WebHook
14 |
15 | Sending WebHook is simply POST request to a given URL. Framework pass all headers and binary body to overlord and you can, for example, chekc authentication.
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/actorapp/actor-bots/ea6c028726b236369b92381462baae752754aedb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Oct 02 02:35:09 MSK 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'actor-bots'
2 | include 'actor-bots-example'
3 |
4 | rootProject.name = 'actor-bots-platform'
5 |
--------------------------------------------------------------------------------