├── .dockerignore ├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .idea └── runConfigurations │ └── Remote_Debug_5005.xml ├── LICENSE ├── README.md ├── baymax-secrets.example.yaml ├── build.gradle ├── buildscript-gradle.lockfile ├── docker ├── Dockerfile └── compose.yaml ├── gradle.lockfile ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── models └── aki.yaml ├── settings.gradle └── src ├── main ├── java │ └── space │ │ └── npstr │ │ └── baymax │ │ ├── Emojis.java │ │ ├── EmojisNumbersParser.java │ │ ├── EventWaiter.java │ │ ├── HelpDeskListener.java │ │ ├── Launcher.java │ │ ├── ModelLoader.java │ │ ├── NodeContext.java │ │ ├── RestActions.java │ │ ├── UserDialogue.java │ │ ├── config │ │ ├── OkHttpConfiguration.java │ │ ├── ShardManagerConfiguration.java │ │ ├── package-info.java │ │ └── properties │ │ │ ├── BaymaxConfig.java │ │ │ └── package-info.java │ │ ├── db │ │ ├── Database.java │ │ ├── TemporaryRoleService.java │ │ └── package-info.java │ │ ├── helpdesk │ │ ├── Branch.java │ │ ├── BranchModel.java │ │ ├── ModelParser.java │ │ ├── Node.java │ │ ├── NodeModel.java │ │ ├── exception │ │ │ ├── MalformedModelException.java │ │ │ ├── MissingTargetNodeException.java │ │ │ ├── NoRootNodeException.java │ │ │ ├── UnreferencedNodesException.java │ │ │ └── package-info.java │ │ └── package-info.java │ │ ├── info │ │ ├── AppInfo.java │ │ ├── GitRepoState.java │ │ └── package-info.java │ │ └── package-info.java └── resources │ ├── META-INF │ └── additional-spring-configuration-metadata.json │ ├── app.properties │ ├── baymax.yaml │ └── db │ └── migrations │ └── V1__AddTemporaryRole.sql └── test ├── java └── space │ └── npstr │ └── baymax │ ├── EmojisNumbersParserTest.java │ └── helpdesk │ └── ModelParserTest.java └── resources └── models ├── branch_loop.yaml ├── branch_loop_large.yaml ├── missing_node.yaml ├── no_root_node.yaml ├── root_loop.yaml ├── sane.yaml └── unreferenced_node.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | #ignore everything except the following files, they are used in the Dockerfile to build the mage 3 | !Dockerfile 4 | !baymax.jar 5 | !build/libs/baymax.jar 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | 14 | - name: Gradle Wrapper Verification 15 | uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 16 | 17 | - name: Set up env 18 | env: 19 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 20 | # Build number is increased by 100 due to legacy reasons from previous CI systems 21 | run: | 22 | echo $GITHUB_RUN_NUMBER 23 | BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER)) 24 | echo $BUILD_NUMBER 25 | echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV" 26 | IMAGE_TAG=$(echo $GITHUB_REF | sed -e 's+refs/heads/++g' | sed -e 's+/+_+g') 27 | echo $IMAGE_TAG 28 | echo "IMAGE_NAME=${{ env.DOCKER_USERNAME }}/baymax:$IMAGE_TAG" >> "$GITHUB_ENV" 29 | 30 | - name: Login to Docker Hub 31 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 32 | with: 33 | username: ${{ secrets.DOCKER_USERNAME }} 34 | password: ${{ secrets.DOCKER_PASSWORD }} 35 | 36 | - name: Setup JDK 37 | uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 38 | with: 39 | distribution: "temurin" 40 | java-version: 21 41 | cache: "gradle" 42 | 43 | - name: Print JDK info 44 | run: java -Xmx32m --version 45 | 46 | - name: Gradle Build 47 | uses: burrunan/gradle-cache-action@c15634bb25b7284dc084f38dff4e838048b7feaf # v1.22 48 | with: 49 | arguments: build --info 50 | 51 | - name: Docker Build 52 | run: docker build -t "${{ env.IMAGE_NAME }}" -f docker/Dockerfile . 53 | 54 | - name: Docker Push 55 | run: docker push "${{ env.IMAGE_NAME }}" 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Git ### 2 | *.orig 3 | 4 | ### Java ### 5 | # Compiled class file 6 | *.class 7 | 8 | # Log file 9 | *.log 10 | 11 | # BlueJ files 12 | *.ctxt 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | 29 | ### Linux ### 30 | *~ 31 | 32 | # temporary files which can be created if a process still has a handle open of a deleted file 33 | .fuse_hidden* 34 | 35 | # KDE directory preferences 36 | .directory 37 | 38 | # Linux trash folder which might appear on any partition or disk 39 | .Trash-* 40 | 41 | # .nfs files are created when an open file is removed but is still being accessed 42 | .nfs* 43 | 44 | ### Gradle ### 45 | .gradle 46 | /build/ 47 | 48 | # Ignore Gradle GUI config 49 | gradle-app.setting 50 | 51 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 52 | !gradle-wrapper.jar 53 | 54 | # Cache of project 55 | .gradletasknamecache 56 | 57 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 58 | # gradle/wrapper/gradle-wrapper.properties 59 | 60 | 61 | # End of https://www.gitignore.io/api/git,java,linux,gradle,intellij+iml 62 | 63 | 64 | baymax-secrets.yaml 65 | application.yaml 66 | logs/ 67 | *.sqlite 68 | dataSources 69 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Remote_Debug_5005.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Baymax 4 | _A friendly help desk bot for Discord_ 5 | 6 | [![Build Status](https://github.com/napstr/Baymax/actions/workflows/build.yaml/badge.svg)](https://github.com/napstr/Baymax/actions/workflows/build.yaml) 7 | 8 | 9 | ### Why? 10 | The job of support staff members of Discord (bot) servers is an annoying task. 11 | Rarely are well-qualified users willing to do this frustrating, repetitive 12 | and more often than not ungrateful chore for longer than a short time. 13 | 14 | Luckily, many of the bad parts can be automated with little effort, 15 | leading to a **faster** and **higher quality** support experience for end users. 16 | 17 | By means of automation, Baymax aims to alleviate the following problem areas from a supporter's point of view: 18 | - Answering the same questions again and again 19 | - Dealing with users who don't state their questions / bug reports / suggestions until someone talks to them 20 | - Dealing with users who are unwilling to read the existing FAQ 21 | - Dealing with users who demand immediate attention by pinging random staff members 22 | - Random spam in a support channel that is read by humans, unnecessarily demanding their attention 23 | 24 | And from a users point of view: 25 | - Receiving immediate help & support for most issues instead of waiting for other humans to show up 26 | - All the information in one place, instead of having to look though FAQs and guides 27 | 28 | ### How? 29 | Baymax uses a simple, yaml based model to define a help desk model as a 30 | [cycle](https://en.wikipedia.org/wiki/Cycle_(graph_theory)) of nodes and branches, 31 | and binds each model to one or many discord channels. 32 | 33 | A sane model has the following properties: 34 | - There is a node with the `root` id 35 | - All branches are targetting existing nodes 36 | - Each defined node can be reached from the `root` node 37 | 38 | 39 | A node may optionally have a role id. Baymax will assign that role id to the user who reaches that node, and remove it again after 3 hours. 40 | 41 | A basic example: 42 | 43 | ```yaml 44 | aki.yaml 45 | 46 | --- 47 | id: root 48 | title: "How may I help you?" 49 | branches: 50 | - message: "I need help with the Aki Bot" 51 | targetId: bot-root 52 | - message: "I need help with the Aki Server" 53 | targetId: server-root 54 | 55 | --- 56 | id: bot-root 57 | title: "You need help with the Aki Bot. What's wrong?" 58 | branches: 59 | - message: "Something is broken" 60 | targetId: bot-broken 61 | - message: "I want to change a setting." 62 | targetId: bot-setting 63 | - message: "I have a suggestion." 64 | targetId: bot-suggestion 65 | - message: "None of the above." 66 | targetId: support-role 67 | 68 | --- 69 | id: server-root 70 | title: "Please DM one of our Moderators." 71 | 72 | --- 73 | id: bot-broken 74 | title: "Aki never breaks." 75 | 76 | --- 77 | id: bot-setting 78 | title: "What setting of the Aki bot do you want to change?" 79 | branches: 80 | - message: "The language" 81 | targetId: bot-setting-language 82 | - message: "Turn reactions on or off" 83 | targetId: bot-setting-reactions 84 | - message: "Restrict it to a single channel" 85 | targetId: bot-setting-restrict 86 | 87 | --- 88 | id: bot-setting-language 89 | title: "Say `!aki lang` to start changing your preferred language." 90 | 91 | --- 92 | id: bot-setting-reactions 93 | title: "Say `!aki thonks disable / enable` to switch reaction on or off." 94 | 95 | --- 96 | id: bot-setting-restrict 97 | title: "Use the Discord permissions to take away Akis write permissions for all the channels where it should not talk in." 98 | 99 | --- 100 | id: bot-suggestion 101 | title: "We don't take any suggestions currently." 102 | 103 | --- 104 | id: support-role 105 | title: "Explain your problem to one of our helpers in <#487925562300694531>." 106 | roleId: 487925645989380108 107 | ``` 108 | 109 | And here is how it looks in chat: 110 | ![](https://ratelimits.are-la.me/aeb692.gif) 111 | 112 | 113 | ### Dependencies: 114 | 115 | - **JDA (Java Discord API)**: 116 | - [Source Code](https://github.com/DV8FromTheWorld/JDA) 117 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) 118 | - [Maven Repository](https://bintray.com/dv8fromtheworld/maven/JDA) 119 | 120 | - **Logback Classic**: 121 | - [Website](https://logback.qos.ch/) 122 | - [Source Code](https://github.com/qos-ch/logback) 123 | - [Logback License](https://logback.qos.ch/license.html) 124 | - [Maven Repository](https://mvnrepository.com/artifact/ch.qos.logback/logback-classic) 125 | 126 | - **Sentry Logback**: 127 | - [Website](https://docs.sentry.io/clients/java/modules/logback/) 128 | - [Source Code](https://github.com/getsentry/sentry-java/tree/master/sentry-logback) 129 | - [BSD 3-Clause License](https://github.com/getsentry/sentry-java/blob/master/LICENSE) 130 | - [Maven Repository](https://mvnrepository.com/artifact/io.sentry/sentry-logback) 131 | 132 | - **OkHttp**: 133 | - [Website](https://square.github.io/okhttp/) 134 | - [Source Code](https://github.com/square/okhttp) 135 | - [Apache 2.0 License](https://square.github.io/okhttp/#license) 136 | - [Maven Repository](https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp) 137 | 138 | - **SnakeYaml**: 139 | - [Source Code](https://bitbucket.org/asomov/snakeyaml) 140 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) 141 | - [Maven Repository](https://mvnrepository.com/artifact/org.yaml/snakeyaml) 142 | 143 | - **Caffeine**: 144 | - [Source Code](https://github.com/ben-manes/caffeine) 145 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.txt) 146 | - [Maven Repository](https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine) 147 | 148 | - **emoji-java**: 149 | - [Source Code](https://github.com/vdurmont/emoji-java) 150 | - [MIT License](http://www.opensource.org/licenses/mit-license.php) 151 | - [Maven Repository](https://mvnrepository.com/artifact/com.vdurmont/emoji-java) 152 | 153 | - **Guava**: 154 | - [Source Code](https://github.com/google/guava) 155 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.txt) 156 | - [Maven Repository](https://mvnrepository.com/artifact/com.google.guava/guava) 157 | 158 | - **SQLite JDBC Driver**: 159 | - [Source Code](https://github.com/xerial/sqlite-jdbc) 160 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.txt) 161 | - [Maven Repository](https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc) 162 | 163 | - **Flyway**: 164 | - [Website](https://flywaydb.org/) 165 | - [Source Code](https://github.com/flyway/flyway) 166 | - [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.txt) 167 | - [Maven Repository](https://mvnrepository.com/artifact/org.flywaydb/flyway-core) 168 | 169 | - **Spring Boot**: 170 | - [Website](https://spring.io/projects/spring-boot) 171 | - [Source Code](https://github.com/spring-projects/spring-boot) 172 | - [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) 173 | - [Maven Repository](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter) 174 | -------------------------------------------------------------------------------- /baymax-secrets.example.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | spring: 4 | output: 5 | ansi: 6 | enabled: always 7 | main: 8 | banner-mode: log 9 | 10 | logging: 11 | file: './logs/baymax.log' 12 | file.max-history: 30 13 | file.max-size: 1GB 14 | 15 | level: 16 | root: INFO 17 | space.npstr: TRACE 18 | 19 | sentry: 20 | dsn: "" 21 | logging.enabled: true 22 | 23 | baymax: 24 | discord-token: "" 25 | status-type: 0 # 0 = "playing", 1 = "streaming", 2 = "listening" 26 | status-message: "a game" 27 | staff-role-ids: 28 | - 479601466110377984 29 | - 242377373947920384 30 | help-desks: 31 | - channel-id: 487005342014636032 32 | model-name: aki 33 | model-uri: https://raw.githubusercontent.com/napstr/Baymax/master/models/aki.yaml 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | buildscript { 4 | configurations.classpath { 5 | resolutionStrategy.activateDependencyLocking() 6 | } 7 | } 8 | 9 | plugins { 10 | id "idea" 11 | id "java" 12 | id "com.gorylenko.gradle-git-properties" version "$gradleGitPluginVersion" 13 | id "com.github.ben-manes.versions" version "$versionsPluginVersion" 14 | id "org.springframework.boot" version "$springBootVersion" 15 | 16 | id "jacoco" 17 | } 18 | 19 | group = 'space.npstr.baymax' 20 | version = '0.0.1-SNAPSHOT' 21 | 22 | java { 23 | toolchain { 24 | languageVersion = JavaLanguageVersion.of(21) 25 | vendor = JvmVendorSpec.ADOPTIUM 26 | } 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | maven { url = 'https://jitpack.io' } 32 | } 33 | 34 | configurations { 35 | // fucks with spring boot jar, we don't need it anyways 36 | compile.exclude module: 'opus-java' 37 | } 38 | 39 | dependencies { 40 | implementation platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion") 41 | testRuntimeOnly platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion") 42 | 43 | implementation "net.dv8tion:JDA:$jdaVersion" 44 | implementation "ch.qos.logback:logback-classic" 45 | implementation "io.sentry:sentry-logback:$sentryVersion" 46 | implementation "io.sentry:sentry-spring-boot-starter-jakarta:$sentryVersion" 47 | implementation "org.yaml:snakeyaml" 48 | implementation "com.github.ben-manes.caffeine:caffeine" 49 | implementation "com.vdurmont:emoji-java:$emojiVersion" 50 | implementation "com.google.guava:guava:$guavaVersion" 51 | implementation "org.xerial:sqlite-jdbc" 52 | implementation "org.flywaydb:flyway-core" 53 | 54 | //spring 55 | implementation "org.springframework.boot:spring-boot-starter" 56 | 57 | //testing 58 | testImplementation "org.junit.jupiter:junit-jupiter-api" 59 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" 60 | testRuntimeOnly "org.junit.platform:junit-platform-launcher" 61 | } 62 | 63 | dependencyUpdates.resolutionStrategy { 64 | componentSelection properReleasesOnly() 65 | } 66 | 67 | dependencyLocking { 68 | lockAllConfigurations() 69 | } 70 | 71 | // ./gradlew resolveAndLockAll --write-locks 72 | task resolveAndLockAll { 73 | doFirst { 74 | assert gradle.startParameter.writeDependencyLocks 75 | } 76 | doLast { 77 | configurations.all { 78 | resolutionStrategy { 79 | componentSelection properReleasesOnly() 80 | } 81 | } 82 | configurations 83 | .findAll { it.canBeResolved } 84 | .each { it.resolve() } 85 | } 86 | } 87 | 88 | tasks.withType(JavaCompile) { 89 | dependsOn(clean, processResources) 90 | options.encoding = 'UTF-8' 91 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 92 | } 93 | 94 | bootRun { 95 | //compiling tests during bootRun increases the likelyhood of catching broken tests locally instead of on the CI 96 | dependsOn compileTestJava 97 | 98 | //pass in custom jvm args 99 | // source: https://stackoverflow.com/a/25079415 100 | // example: ./gradlew bootRun -PjvmArgs="--illegal-access=debug -Dwhatever=value" 101 | if (project.hasProperty('jvmArgs')) { 102 | //noinspection GroovyAssignabilityCheck 103 | jvmArgs project.jvmArgs.split('\\s+') 104 | } 105 | } 106 | 107 | bootJar { 108 | archiveFileName.set("baymax.jar") 109 | doLast { 110 | copy { 111 | from 'build/libs/baymax.jar' 112 | into '.' 113 | } 114 | } 115 | } 116 | 117 | test { 118 | useJUnitPlatform() 119 | jacoco { 120 | includes['space.npstr.baymax.*'] 121 | } 122 | } 123 | 124 | processResources { 125 | //inject values into app.properties 126 | def projectVersion = project.version 127 | def projectGroup = project.group 128 | def projectName = project.name 129 | filesMatching("**/app.properties") { 130 | filter ReplaceTokens, tokens: [ 131 | "project.version" : projectVersion, 132 | "project.groupId" : projectGroup, 133 | "project.artifactId": projectName, 134 | "env.BUILD_NUMBER" : (System.getenv('CI') ? System.getenv('BUILD_NUMBER') : 'DEV'), 135 | "env.BUILD_TIME" : System.currentTimeMillis() + '' 136 | ] 137 | } 138 | } 139 | 140 | static def properReleasesOnly() { 141 | return { rules -> 142 | rules.all { ComponentSelection selection -> 143 | boolean rejected = [ 144 | 'alpha', 'beta', 'rc', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'preview', 145 | ].any { 146 | q -> selection.candidate.version.toLowerCase().contains(q) && !selection.candidate.module.equals("JDA") 147 | } 148 | if (rejected) { 149 | selection.reject('Not a release') 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /buildscript-gradle.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.fasterxml.jackson.core:jackson-annotations:2.18.3=classpath 5 | com.fasterxml.jackson.core:jackson-core:2.18.3=classpath 6 | com.fasterxml.jackson.core:jackson-databind:2.18.3=classpath 7 | com.fasterxml.jackson.module:jackson-module-parameter-names:2.18.3=classpath 8 | com.fasterxml.jackson:jackson-bom:2.18.3=classpath 9 | com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.52.0=classpath 10 | com.github.ben-manes:gradle-versions-plugin:0.52.0=classpath 11 | com.google.code.findbugs:jsr305:3.0.2=classpath 12 | com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin:2.5.0=classpath 13 | com.gorylenko.gradle-git-properties:gradle-git-properties:2.5.0=classpath 14 | com.squareup.moshi:moshi-kotlin:1.12.0=classpath 15 | com.squareup.moshi:moshi:1.12.0=classpath 16 | com.squareup.okhttp3:okhttp:4.12.0=classpath 17 | com.squareup.okio:okio-jvm:3.6.0=classpath 18 | com.squareup.okio:okio:3.6.0=classpath 19 | io.spring.gradle:dependency-management-plugin:1.1.7=classpath 20 | net.java.dev.jna:jna-platform:5.13.0=classpath 21 | net.java.dev.jna:jna:5.13.0=classpath 22 | org.antlr:antlr4-runtime:4.7.2=classpath 23 | org.apache.commons:commons-compress:1.25.0=classpath 24 | org.apache.httpcomponents.client5:httpclient5:5.4.3=classpath 25 | org.apache.httpcomponents.core5:httpcore5-h2:5.3.4=classpath 26 | org.apache.httpcomponents.core5:httpcore5:5.3.4=classpath 27 | org.jetbrains.kotlin:kotlin-bom:2.0.21=classpath 28 | org.jetbrains.kotlin:kotlin-reflect:2.0.21=classpath 29 | org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21=classpath 30 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21=classpath 31 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21=classpath 32 | org.jetbrains.kotlin:kotlin-stdlib:2.0.21=classpath 33 | org.jetbrains:annotations:13.0=classpath 34 | org.slf4j:slf4j-api:1.7.36=classpath 35 | org.springframework.boot:org.springframework.boot.gradle.plugin:3.4.5=classpath 36 | org.springframework.boot:spring-boot-buildpack-platform:3.4.5=classpath 37 | org.springframework.boot:spring-boot-gradle-plugin:3.4.5=classpath 38 | org.springframework.boot:spring-boot-loader-tools:3.4.5=classpath 39 | org.springframework:spring-core:6.2.6=classpath 40 | org.springframework:spring-jcl:6.2.6=classpath 41 | org.tomlj:tomlj:1.0.0=classpath 42 | empty= 43 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21-jdk 2 | LABEL org.opencontainers.image.authors="napster@npstr.space" 3 | 4 | ENV ENV=docker 5 | 6 | WORKDIR /opt/baymax 7 | 8 | RUN touch baymax.sqlite 9 | 10 | ENTRYPOINT ["java", "-Xmx256m", "-jar", "baymax.jar"] 11 | 12 | COPY build/libs/baymax.jar /opt/baymax/baymax.jar 13 | -------------------------------------------------------------------------------- /docker/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | bot: 4 | image: napstr/baymax:master 5 | restart: always 6 | labels: 7 | - "com.centurylinklabs.watchtower.enable=true" 8 | ports: 9 | - 127.0.0.1:5007:5005 # Remote debug 10 | volumes: 11 | - "./baymax-secrets.yaml:/opt/baymax/baymax-secrets.yaml:ro" 12 | - "./models:/opt/baymax/models:ro" 13 | - "./logs:/opt/baymax/logs" 14 | - "./logs/gc:/opt/baymax/logs/gc" 15 | - "./baymax.sqlite:/opt/baymax/baymax.sqlite" 16 | entrypoint: 17 | - java 18 | - -Xms512m 19 | - -Xmx512m 20 | - -XX:+AlwaysPreTouch 21 | - -XX:+HeapDumpOnOutOfMemoryError 22 | - -XX:HeapDumpPath=emergencydump.hprof 23 | - -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 24 | - -Xlog:gc*:logs/gc/gc-%t.log::filesize=1g 25 | #- -XX:+UseG1GC 26 | #- -XX:+ParallelRefProcEnabled 27 | - -XX:+UseZGC 28 | - -XX:+ZGenerational 29 | - -XX:+UseTransparentHugePages 30 | - -XX:+UseStringDeduplication 31 | - -jar 32 | - baymax.jar 33 | 34 | #watchtower: 35 | # image: containrrr/watchtower 36 | # restart: always 37 | # labels: 38 | # - "com.centurylinklabs.watchtower.enable=true" 39 | # volumes: 40 | # - /var/run/docker.sock:/var/run/docker.sock 41 | # command: --cleanup --label-enable --interval 300 #seconds 42 | -------------------------------------------------------------------------------- /gradle.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | ch.qos.logback:logback-classic:1.5.18=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 5 | ch.qos.logback:logback-core:1.5.18=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 6 | club.minnced:opus-java-api:1.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 7 | club.minnced:opus-java-natives:1.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 8 | club.minnced:opus-java:1.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 9 | com.fasterxml.jackson.core:jackson-annotations:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 10 | com.fasterxml.jackson.core:jackson-core:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 11 | com.fasterxml.jackson.core:jackson-databind:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 12 | com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 13 | com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 14 | com.fasterxml.jackson:jackson-bom:2.18.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 15 | com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 16 | com.google.code.findbugs:jsr305:3.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 17 | com.google.code.gson:gson:2.11.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 18 | com.google.crypto.tink:tink:1.17.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 19 | com.google.errorprone:error_prone_annotations:2.36.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 20 | com.google.guava:failureaccess:1.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 21 | com.google.guava:guava:33.4.8-jre=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 22 | com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 23 | com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 24 | com.google.protobuf:protobuf-java:4.28.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 25 | com.neovisionaries:nv-websocket-client:2.14=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 26 | com.squareup.okhttp3:okhttp:4.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 27 | com.squareup.okio:okio-jvm:3.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 28 | com.squareup.okio:okio:3.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 29 | com.vdurmont:emoji-java:5.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 30 | io.micrometer:micrometer-commons:1.14.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 31 | io.micrometer:micrometer-observation:1.14.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 32 | io.sentry:sentry-logback:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 33 | io.sentry:sentry-reactor:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 34 | io.sentry:sentry-spring-boot-jakarta:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 35 | io.sentry:sentry-spring-boot-starter-jakarta:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 36 | io.sentry:sentry-spring-jakarta:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 37 | io.sentry:sentry:8.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 38 | jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 39 | net.dv8tion:JDA:5.5.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 40 | net.java.dev.jna:jna:4.4.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 41 | net.sf.trove4j:core:3.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath 42 | org.apache.commons:commons-collections4:4.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 43 | org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 44 | org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 45 | org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath 46 | org.checkerframework:checker-qual:3.37.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 47 | org.flywaydb:flyway-core:10.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 48 | org.jacoco:org.jacoco.agent:0.8.13=jacocoAgent,jacocoAnt 49 | org.jacoco:org.jacoco.ant:0.8.13=jacocoAnt 50 | org.jacoco:org.jacoco.core:0.8.13=jacocoAnt 51 | org.jacoco:org.jacoco.report:0.8.13=jacocoAnt 52 | org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 53 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 54 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 55 | org.jetbrains.kotlin:kotlin-stdlib:1.9.25=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 56 | org.jetbrains:annotations:13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 57 | org.json:json:20170516=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 58 | org.jspecify:jspecify:1.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 59 | org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testRuntimeClasspath 60 | org.junit.jupiter:junit-jupiter-engine:5.11.4=testRuntimeClasspath 61 | org.junit.platform:junit-platform-commons:1.11.4=testCompileClasspath,testRuntimeClasspath 62 | org.junit.platform:junit-platform-engine:1.11.4=testRuntimeClasspath 63 | org.junit.platform:junit-platform-launcher:1.11.4=testRuntimeClasspath 64 | org.junit:junit-bom:5.11.4=testCompileClasspath,testRuntimeClasspath 65 | org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath 66 | org.ow2.asm:asm-commons:9.8=jacocoAnt 67 | org.ow2.asm:asm-tree:9.8=jacocoAnt 68 | org.ow2.asm:asm:9.8=jacocoAnt 69 | org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 70 | org.slf4j:slf4j-api:2.0.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 71 | org.springframework.boot:spring-boot-autoconfigure:3.4.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 72 | org.springframework.boot:spring-boot-dependencies:3.4.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 73 | org.springframework.boot:spring-boot-starter-logging:3.4.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 74 | org.springframework.boot:spring-boot-starter:3.4.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 75 | org.springframework.boot:spring-boot:3.4.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 76 | org.springframework:spring-aop:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 77 | org.springframework:spring-beans:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 78 | org.springframework:spring-context:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 79 | org.springframework:spring-core:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 80 | org.springframework:spring-expression:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 81 | org.springframework:spring-jcl:6.2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 82 | org.xerial:sqlite-jdbc:3.47.2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 83 | org.yaml:snakeyaml:2.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 84 | empty=annotationProcessor,compile,developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor 85 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.warning.mode=all 2 | 3 | springBootVersion=[3.4.0, 3.5[ 4 | 5 | gradleGitPluginVersion=[2.4, 3.0[ 6 | versionsPluginVersion=0.+ 7 | 8 | jdaVersion=[5.5.1, 5.6[ 9 | sentryVersion=[8.0.0, 9.0[ 10 | emojiVersion=[5.1.1, 6.0[ 11 | guavaVersion=[33.4.0-jre, 34.0[ 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CapybaraLabs/Baymax/aed896571783a4526fcbc0a91467d36d7489d9f6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=efe9a3d147d948d7528a9887fa35abcf24ca1a43ad06439996490f77569b02d1 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /models/aki.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "How may I help you?" 4 | branches: 5 | - message: "I need help with the Aki Bot" 6 | targetId: bot-root 7 | - message: "I need help with the Aki Server" 8 | targetId: server-root 9 | 10 | 11 | ### Bot stuff 12 | 13 | --- 14 | id: bot-root 15 | title: "You need help with Aki the bot. What's wrong?" 16 | branches: 17 | - message: "Something is broken." 18 | targetId: bot-broken 19 | - message: "I want to change a setting." 20 | targetId: bot-setting 21 | - message: "I have a question about Aki." 22 | targetId: bot-question 23 | - message: "I have a suggestion." 24 | targetId: bot-suggestion 25 | - message: "I have looked at **all** the options, and none of them helped me." 26 | targetId: support-role 27 | 28 | --- 29 | id: bot-broken 30 | title: "What about Aki is broken?" 31 | branches: 32 | - message: "Aki is not online." 33 | targetId: bot-broken-not-online 34 | - message: "Aki isn't responding." 35 | targetId: bot-broken-not-responding 36 | - message: "Aki is sending blank messages." 37 | targetId: bot-broken-blank-messages 38 | - message: "Aki keeps asking for a vote, but I have voted already." 39 | targetId: bot-broken-votes 40 | - message: "Something else is broken" 41 | targetId: support-role 42 | 43 | --- 44 | id: bot-broken-not-online 45 | title: "Is Aki present in your server at all?" 46 | branches: 47 | - message: "Aki is not present in my server." 48 | targetId: bot-invite 49 | - message: "Aki is in my server, but is shown as offline." 50 | targetId: bot-broken-offline 51 | 52 | --- 53 | id: bot-invite 54 | title: "To add the bot to your own server, navigate to this link: " 55 | 56 | --- 57 | id: bot-broken-offline 58 | title: "Please read <#473155871959023636> and pay attention to any pinned, as well as recent messages." # #bot-status 59 | branches: 60 | - message: "I found an explanation for the downtime in <#473155871959023636>" # #bot-status 61 | targetId: root 62 | - message: "There is no announced or scheduled downtime for Aki to be found in <#473155871959023636>" # #bot-status 63 | targetId: let-us-know 64 | 65 | --- 66 | id: let-us-know 67 | title: "Please let us know about this in <#487957025012318218>." # #support 68 | roleId: 487925645989380108 # #support 69 | 70 | --- 71 | id: bot-broken-not-responding 72 | title: "If Aki is not responding, did you maybe change the prefix? Say `@Aki#9694 prefix` to check Aki's current prefix." 73 | 74 | --- 75 | id: bot-broken-blank-messages 76 | title: "If Aki is sending blank messages, then you do not have the `Link Preview` option enabled.\n 77 | In your Discord client, navigate to Settings > Text & Images and enable the option `Show website preview info from links pasted into chat`.\n 78 | https://imgur.com/a/5uoVsgl" 79 | 80 | --- 81 | id: bot-broken-votes 82 | title: "Try this command: `!aki votecheck`. Did that help?" 83 | branches: 84 | - message: "Yes! Aki recognizes by vote now." 85 | targetId: root 86 | - message: "No. Aki still says that I have not voted" 87 | targetId: bot-broken-not-voted 88 | 89 | --- 90 | id: bot-broken-not-voted 91 | title: "Make sure you are logged in with the exact same account on the website, as you are using in your Discord client. 92 | You can see your logged in account by going to ." 93 | branches: 94 | - message: "Woops, I was logged into the wrong account. I have logged into the correct one and voted, and things are working." 95 | targetId: root 96 | - message: "Nope, I am 100% sure that I am logged into the same account." 97 | targetId: let-us-know 98 | 99 | --- 100 | id: bot-setting 101 | title: "What setting of the Aki bot do you want to change?" 102 | branches: 103 | - message: "The language" 104 | targetId: bot-setting-language 105 | - message: "Turn reactions on or off" 106 | targetId: bot-setting-reactions 107 | - message: "The prefix" 108 | targetId: bot-setting-prefix 109 | - message: "Restrict it to a single channel" 110 | targetId: bot-setting-restrict 111 | 112 | --- 113 | id: bot-setting-language 114 | title: "Say `!aki lang` to start changing your preferred language. If you want to change the default language for the entire server, you can add `--guild` to the end of the command, like so: `!aki lang en --guild`" 115 | 116 | --- 117 | id: bot-setting-reactions 118 | title: "Say `!aki thonks disable / enable` to switch reaction on or off." 119 | 120 | --- 121 | id: bot-setting-prefix 122 | title: "Say `!aki prefix ` to change Aki's prefix. Say `@Aki#9694 prefix reset` to reset Aki's prefix." 123 | 124 | --- 125 | id: bot-setting-restrict 126 | title: "Use the Discord permissions to take away Akis write permissions for all the channels where it should not talk in." 127 | 128 | --- 129 | id: bot-question 130 | title: "What question do you have about the bot?" 131 | branches: 132 | - message: "How do I add a character to Aki?" 133 | targetId: bot-question-add-character 134 | - message: "How do I add Aki to my server?" 135 | targetId: bot-invite 136 | - message: "How do I continue my game after upvoting?" 137 | targetId: bot-question-upvote-continue 138 | - message: "Why do I have to upvote to continue playing Aki past 20 questions?" 139 | targetId: bot-question-upvote-why 140 | - message: "What is Evil Aki?" 141 | targetId: bot-question-evil-aki 142 | 143 | --- 144 | id: bot-question-add-character 145 | title: "The Aki bot runs off the Akinator database, so to add a character, you would need to add it there.\n 146 | Link: " 147 | 148 | --- 149 | id: bot-question-upvote-continue 150 | title: "Once you've upvoted, you should receive a DM from Aki confirming that he received your upvote. At that point, you can continue your game.\n 151 | - If you're unable to, type `!aki votecheck` to run a manual check for your vote.\n 152 | - If Aki can't find your vote, make sure you're logged into the same account on the browser version of Discord." 153 | 154 | --- 155 | id: bot-question-upvote-why 156 | title: "Contrary to popular belief, Aki does not run on thin air. It runs on servers and time invested. Servers cost money; a lot more money than you think servers would cost. The time it takes to ensure everything works and looks nice is far more complicated than others may believe.\n 157 | \n 158 | These are your options:\n 159 | - Support the bot's upkeep by becoming a patron: \n 160 | - Upvote the bot every day.\n 161 | - Don't use it." 162 | 163 | --- 164 | id: bot-question-evil-aki 165 | title: "Evil Aki is the evil version of Aki. Duh.\n 166 | It's actually a debug/test bot to test changes and new features before committing them to the actual bot." 167 | 168 | --- 169 | id: bot-suggestion 170 | title: "We don't take any suggestions currently." 171 | 172 | 173 | ### Server stuff 174 | 175 | --- 176 | id: server-root 177 | title: "You need help with the Aki Server. What's wrong?" 178 | branches: 179 | - message: "Someone from this server sent me ads or inappropriate messages in my private messages." 180 | targetId: report-user 181 | - message: "Someone is breaking the rules!" 182 | targetId: report-user 183 | - message: "I can't type in one of the topic channels (art, gaming, etc). How do I get access?" 184 | targetId: topic-key 185 | - message: "I can't set or change my nick. How can I change my nick in this place?" 186 | targetId: change-nick 187 | - message: "I want to advertise my community / How do I post in <#486479857732157441>?" # #sponsors 188 | targetId: sponsors 189 | - message: "I want to join staff / become a moderator." 190 | targetId: join-staff 191 | - message: "I have looked at **all** the options, and none of them helped me." 192 | targetId: support-role 193 | 194 | --- 195 | id: report-user 196 | title: "Please report this in <#380426950818398208>, if possible including screenshots." # #support 197 | roleId: 487925645989380108 # #support 198 | 199 | --- 200 | id: topic-key 201 | title: "Ask any staff member to get an access key role for the topic channels (art, gaming, etc)." 202 | 203 | --- 204 | id: change-nick 205 | title: "Be more active by participating in our general or topic channels. Over time, you will gain a role that gives you the permission to set and change your nick." 206 | 207 | --- 208 | id: sponsors 209 | title: "We are happy to promote those communities that significantly support the Aki bot.\n 210 | Sign up for the sponsor tier at our Patreon , and you will get access to post in <#486479857732157441>." # #sponsors 211 | 212 | --- 213 | id: join-staff 214 | title: "To join our staff, show some activity and maturity in our chats. We value meaningful and positive contributions to our community, 215 | and will take notice of that and contact you. Alternatively, about every six months, we may open staff applications. Keep an eye out for 216 | that in <#472461981199237140> (we won't be pinging)." # #server-news 217 | 218 | --- 219 | id: support-role 220 | title: "Please explain your problem to one of our helpers in <#380426950818398208>." # #support 221 | roleId: 487925645989380108 # #support 222 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.name = 'baymax' 9 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/Emojis.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import com.vdurmont.emoji.Emoji; 21 | import com.vdurmont.emoji.EmojiManager; 22 | 23 | /** 24 | * Created by napster on 05.09.18. 25 | */ 26 | public class Emojis { 27 | 28 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Emojis.class); 29 | 30 | private Emojis() {} 31 | 32 | public static String get(String alias) { 33 | Emoji emoji = EmojiManager.getForAlias(alias); 34 | if (emoji == null) { 35 | log.warn("Emoji not found for alias {}, falling back to :thinking:", alias); 36 | emoji = EmojiManager.getForAlias("thinking"); 37 | } 38 | return emoji.getUnicode(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/EmojisNumbersParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2019 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import java.util.Map; 21 | import java.util.Optional; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * Parse Emojis into a number 26 | */ 27 | public class EmojisNumbersParser { 28 | 29 | public static final String ZERO = Emojis.get("zero"); 30 | public static final String ONE = Emojis.get("one"); 31 | public static final String TWO = Emojis.get("two"); 32 | public static final String THREE = Emojis.get("three"); 33 | public static final String FOUR = Emojis.get("four"); 34 | public static final String FIVE = Emojis.get("five"); 35 | public static final String SIX = Emojis.get("six"); 36 | public static final String SEVEN = Emojis.get("seven"); 37 | public static final String EIGHT = Emojis.get("eight"); 38 | public static final String NINE = Emojis.get("nine"); 39 | 40 | private static final Map EMOJI_TO_DIGIT = Map.of( 41 | ZERO, '0', 42 | ONE, '1', 43 | TWO, '2', 44 | THREE, '3', 45 | FOUR, '4', 46 | FIVE, '5', 47 | SIX, '6', 48 | SEVEN, '7', 49 | EIGHT, '8', 50 | NINE, '9' 51 | ); 52 | 53 | private static final Map DIGIT_TO_EMOJI = EMOJI_TO_DIGIT.entrySet().stream() 54 | .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); 55 | 56 | 57 | public String numberAsEmojis(int number) { 58 | String numberAsString = Integer.toString(number); 59 | return numberAsString.chars() 60 | .mapToObj(c -> digitToEmoji((char) c)) 61 | .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) 62 | .toString(); 63 | } 64 | 65 | public String digitToEmoji(char digit) { 66 | String result = DIGIT_TO_EMOJI.get(digit); 67 | if (result == null || result.isEmpty()) { 68 | throw new NumberFormatException(digit + " is not a digit"); 69 | } 70 | return result; 71 | } 72 | 73 | 74 | public Optional emojisToNumber(String input) { 75 | try { 76 | return Optional.of(Integer.parseInt(parse(input))); 77 | } catch (NumberFormatException e) { 78 | return Optional.empty(); 79 | } 80 | } 81 | 82 | private String parse(String input) { 83 | if (input.isEmpty()) return ""; 84 | EmojiMapping found = startsWithNumberEmoji(input); 85 | String shortened = input.replaceFirst(found.emoji, ""); 86 | return found.digit + parse(shortened); 87 | } 88 | 89 | private EmojiMapping startsWithNumberEmoji(String input) { 90 | return EMOJI_TO_DIGIT.entrySet().stream() 91 | .filter(entry -> input.startsWith(entry.getKey())) 92 | .findAny() 93 | .map(entry -> new EmojiMapping(entry.getKey(), entry.getValue())) 94 | .orElseThrow(() -> new NumberFormatException("Input " + input + " does not start with a known emoji.")); 95 | } 96 | 97 | private static class EmojiMapping { 98 | public final String emoji; 99 | public final char digit; 100 | 101 | public EmojiMapping(String emoji, char digit) { 102 | this.emoji = emoji; 103 | this.digit = digit; 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/EventWaiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import net.dv8tion.jda.api.events.GenericEvent; 21 | import net.dv8tion.jda.api.hooks.EventListener; 22 | import org.springframework.stereotype.Component; 23 | 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Set; 29 | import java.util.concurrent.ScheduledExecutorService; 30 | import java.util.concurrent.ScheduledThreadPoolExecutor; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.function.Consumer; 33 | import java.util.function.Predicate; 34 | 35 | /** 36 | * Created by napster on 05.09.18. 37 | *

38 | * Just like the JDA-Utils EventWaiter (Apache v2) but a lot less crappy aka 39 | * - threadsafe 40 | * - doesn't block the main JDA threads 41 | * - efficient 42 | * - stricter types 43 | * - doesn't have to wait 6 months (and counting) for fixes 44 | */ 45 | @Component 46 | public class EventWaiter implements EventListener { 47 | 48 | //this thread pool runs the actions as well as the timeout actions 49 | private final ScheduledExecutorService pool; 50 | //modifications to the hash map and sets have to go through this single threaded pool 51 | private final ScheduledExecutorService single; 52 | 53 | //These stateful collections are only threadsafe when modified though the single executor 54 | private final List> toRemove = new ArrayList<>(); //reused object 55 | private final HashMap, Set>> waitingEvents; 56 | 57 | public EventWaiter(ScheduledThreadPoolExecutor jdaThreadPool) { 58 | this.waitingEvents = new HashMap<>(); 59 | this.pool = jdaThreadPool; 60 | this.single = new ScheduledThreadPoolExecutor(1); 61 | } 62 | 63 | public EventWaiter.WaitingEvent waitForEvent( 64 | Class classType, Predicate condition, Consumer action, long timeout, TimeUnit unit, 65 | Runnable timeoutAction 66 | ) { 67 | 68 | EventWaiter.WaitingEvent we = new EventWaiter.WaitingEvent<>(condition, action); 69 | 70 | this.single.execute(() -> { 71 | this.waitingEvents.computeIfAbsent(classType, c -> new HashSet<>()) 72 | .add(we); 73 | this.single.schedule(() -> { 74 | var set = this.waitingEvents.get(classType); 75 | if (set == null) { 76 | return; 77 | } 78 | if (set.remove(we)) { 79 | this.pool.execute(timeoutAction); 80 | } 81 | 82 | if (set.isEmpty()) { 83 | this.waitingEvents.remove(classType); 84 | } 85 | }, timeout, unit); 86 | }); 87 | return we; 88 | } 89 | 90 | @Override 91 | public final void onEvent(GenericEvent event) { 92 | Class cc = event.getClass(); 93 | 94 | // Runs at least once for the fired Event, at most 95 | // once for each superclass (excluding Object) because 96 | // Class#getSuperclass() returns null when the superclass 97 | // is primitive, void, or (in this case) Object. 98 | while (cc != null && cc != Object.class) { 99 | Class clazz = cc; 100 | if (this.waitingEvents.get(clazz) != null) { 101 | this.single.execute(() -> { 102 | Set> set = this.waitingEvents.get(clazz); 103 | @SuppressWarnings({"unchecked", "rawtypes", "java:S3740"}) Predicate filter = we -> we.attempt(event); 104 | set.stream().filter(filter).forEach(this.toRemove::add); 105 | set.removeAll(this.toRemove); 106 | this.toRemove.clear(); 107 | 108 | if (set.isEmpty()) { 109 | this.waitingEvents.remove(clazz); 110 | } 111 | }); 112 | } 113 | 114 | cc = cc.getSuperclass(); 115 | } 116 | } 117 | 118 | public class WaitingEvent { 119 | final Predicate condition; 120 | final Consumer action; 121 | 122 | WaitingEvent(Predicate condition, Consumer action) { 123 | this.condition = condition; 124 | this.action = action; 125 | } 126 | 127 | private boolean attempt(T event) { 128 | if (this.condition.test(event)) { 129 | EventWaiter.this.pool.execute(() -> this.action.accept(event)); 130 | return true; 131 | } 132 | return false; 133 | } 134 | 135 | public void cancel() { 136 | EventWaiter.this.single.execute( 137 | () -> EventWaiter.this.waitingEvents.values().forEach(set -> set.remove(this)) 138 | ); 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/HelpDeskListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2022 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import com.github.benmanes.caffeine.cache.Cache; 21 | import com.github.benmanes.caffeine.cache.Caffeine; 22 | import com.github.benmanes.caffeine.cache.RemovalCause; 23 | import java.net.URI; 24 | import java.time.Duration; 25 | import java.util.Map; 26 | import java.util.Objects; 27 | import java.util.Optional; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | import java.util.concurrent.TimeUnit; 30 | import net.dv8tion.jda.api.entities.Member; 31 | import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 32 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 33 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 34 | import net.dv8tion.jda.api.events.session.ReadyEvent; 35 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 36 | import net.dv8tion.jda.api.sharding.ShardManager; 37 | import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; 38 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 39 | import org.springframework.lang.Nullable; 40 | import org.springframework.stereotype.Component; 41 | import org.yaml.snakeyaml.error.YAMLException; 42 | import space.npstr.baymax.config.properties.BaymaxConfig; 43 | import space.npstr.baymax.db.TemporaryRoleService; 44 | import space.npstr.baymax.helpdesk.Node; 45 | import space.npstr.baymax.helpdesk.exception.MalformedModelException; 46 | 47 | /** 48 | * Created by napster on 05.09.18. 49 | */ 50 | @Component 51 | public class HelpDeskListener extends ListenerAdapter { 52 | 53 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HelpDeskListener.class); 54 | 55 | public static final int EXPIRE_MINUTES = 2; 56 | 57 | private final EventWaiter eventWaiter; 58 | private final ModelLoader modelLoader; 59 | private final BaymaxConfig baymaxConfig; 60 | private final RestActions restActions; 61 | private final TemporaryRoleService temporaryRoleService; 62 | 63 | //channel id of the helpdesk <-> user id <-> ongoing dialogue 64 | private final Map> helpDesksDialogues = new ConcurrentHashMap<>(); 65 | 66 | public HelpDeskListener(EventWaiter eventWaiter, ModelLoader modelLoader, BaymaxConfig baymaxConfig, 67 | RestActions restActions, TemporaryRoleService temporaryRoleService) { 68 | 69 | this.eventWaiter = eventWaiter; 70 | this.modelLoader = modelLoader; 71 | this.baymaxConfig = baymaxConfig; 72 | this.restActions = restActions; 73 | this.temporaryRoleService = temporaryRoleService; 74 | } 75 | 76 | @Override 77 | public void onMessageReceived(MessageReceivedEvent event) { 78 | MessageChannel messageChannel = event.getChannel(); 79 | if (!(messageChannel instanceof TextChannel channel) || !messageChannel.canTalk()) { 80 | return; 81 | } 82 | 83 | var helpDeskOpt = this.baymaxConfig.helpDesks().stream() 84 | .filter(helpDesk -> helpDesk.channelId() == channel.getIdLong()) 85 | .findAny(); 86 | 87 | if (helpDeskOpt.isEmpty()) { 88 | return; 89 | } 90 | if (event.getAuthor().isBot()) { 91 | if (event.getAuthor().getIdLong() == event.getJDA().getSelfUser().getIdLong()) { 92 | return; 93 | } 94 | 95 | restActions.deleteMessageAfter(event.getMessage(), Duration.ofSeconds(5)) 96 | .whenComplete((__, t) -> { 97 | if (t != null) { 98 | log.error("Failed to delete bot message in channel {}", channel, t); 99 | } 100 | }); 101 | return; 102 | } 103 | 104 | var helpDesk = helpDeskOpt.get(); 105 | var userDialogues = this.helpDesksDialogues.computeIfAbsent( 106 | helpDesk.channelId(), channelId -> this.createUserDialogueCache() 107 | ); 108 | Member member = event.getMember(); 109 | if (member == null) { 110 | return; 111 | } 112 | if (isStaff(member)) { 113 | if (event.getMessage().getMentions().isMentioned(event.getJDA().getSelfUser())) { 114 | String content = event.getMessage().getContentRaw().toLowerCase(); 115 | if (content.contains("init")) { 116 | userDialogues.invalidateAll(); 117 | userDialogues.cleanUp(); 118 | init(channel, helpDesk.modelName(), helpDesk.modelUri()); 119 | return; 120 | } else if (content.contains("reload")) { 121 | try { 122 | var reloadedModel = this.modelLoader.attemptReload(helpDesk.modelName(), helpDesk.modelUri()); 123 | userDialogues.invalidateAll(); 124 | userDialogues.cleanUp(); 125 | init(channel, reloadedModel); 126 | } catch (MalformedModelException | YAMLException e) { 127 | MessageCreateData message = new MessageCreateBuilder().addContent("Failed to load model due to: **") 128 | .addContent(e.getMessage()) 129 | .addContent("**") 130 | .build(); 131 | this.restActions.sendMessage(channel, message) 132 | .whenComplete((__, t) -> { 133 | if (t != null) { 134 | log.error("Failed to reply in channel {}", channel, t); 135 | } 136 | }); 137 | } 138 | return; 139 | } 140 | } 141 | } 142 | 143 | userDialogues.get(event.getAuthor().getIdLong(), 144 | userId -> { 145 | var model = this.modelLoader.getModel(helpDesk.modelName(), helpDesk.modelUri()); 146 | return new UserDialogue(this.eventWaiter, model, event, this.restActions, this.temporaryRoleService); 147 | }); 148 | } 149 | 150 | // revisit for when there is more than one shard 151 | @Override 152 | public void onReady(ReadyEvent event) { 153 | //1. Clean up the channel 154 | //2. Post the root message 155 | 156 | ShardManager shardManager = Objects.requireNonNull(event.getJDA().getShardManager(), "Shard manager required"); 157 | for (BaymaxConfig.HelpDesk helpDesk : this.baymaxConfig.helpDesks()) { 158 | TextChannel channel = shardManager.getTextChannelById(helpDesk.channelId()); 159 | if (channel == null) { 160 | log.warn("Failed to find and setup configured help desk channel {}", helpDesk.channelId()); 161 | return; 162 | } 163 | init(channel, helpDesk.modelName(), helpDesk.modelUri()); 164 | } 165 | } 166 | 167 | private void init(TextChannel channel, String modelName, @Nullable URI modelUri) { 168 | init(channel, this.modelLoader.getModel(modelName, modelUri)); 169 | } 170 | 171 | private void init(TextChannel channel, Map model) { 172 | try { 173 | this.restActions.purgeChannel(channel) 174 | .exceptionally(t -> { 175 | log.error("Failed to purge messages for init in channel {}", channel, t); 176 | return null; //Void 177 | }) 178 | .thenCompose(__ -> { 179 | NodeContext nodeContext = new NodeContext(model.get("root"), Optional.empty()); 180 | MessageCreateData message = UserDialogue.asMessage(nodeContext); 181 | return this.restActions.sendMessage(channel, message); 182 | }) 183 | .whenComplete((__, t) -> { 184 | if (t != null) { 185 | log.error("Failed to send init message in channel {}", channel, t); 186 | } 187 | }); 188 | } catch (Exception e) { 189 | log.error("Failed to purge channel {}", channel, e); 190 | } 191 | } 192 | 193 | private boolean isStaff(Member member) { 194 | return member.getRoles().stream() 195 | .anyMatch(role -> this.baymaxConfig.staffRoleIds().contains(role.getIdLong())); 196 | } 197 | 198 | private Cache createUserDialogueCache() { 199 | return Caffeine.newBuilder() 200 | .expireAfterAccess(EXPIRE_MINUTES, TimeUnit.MINUTES) 201 | .removalListener((@Nullable Long userId, @Nullable UserDialogue userDialogue, RemovalCause cause) -> { 202 | if (userDialogue != null) { 203 | userDialogue.done(); 204 | } 205 | }) 206 | .build(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/Launcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import jakarta.annotation.PreDestroy; 21 | import java.time.Instant; 22 | import java.time.ZoneId; 23 | import java.time.format.DateTimeFormatter; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.concurrent.ScheduledThreadPoolExecutor; 27 | import java.util.concurrent.TimeUnit; 28 | import net.dv8tion.jda.api.JDAInfo; 29 | import net.dv8tion.jda.api.sharding.ShardManager; 30 | import org.springframework.beans.factory.ObjectProvider; 31 | import org.springframework.boot.ApplicationArguments; 32 | import org.springframework.boot.ApplicationRunner; 33 | import org.springframework.boot.SpringApplication; 34 | import org.springframework.boot.autoconfigure.SpringBootApplication; 35 | import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; 36 | import org.springframework.boot.context.event.ApplicationFailedEvent; 37 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 38 | import space.npstr.baymax.config.properties.BaymaxConfig; 39 | import space.npstr.baymax.info.AppInfo; 40 | import space.npstr.baymax.info.GitRepoState; 41 | 42 | /** 43 | * Created by napster on 05.09.18. 44 | */ 45 | @SpringBootApplication 46 | @EnableConfigurationProperties({ 47 | BaymaxConfig.class, 48 | }) 49 | public class Launcher implements ApplicationRunner { 50 | 51 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Launcher.class); 52 | 53 | private final Thread shutdownHook; 54 | private volatile boolean shutdownHookAdded = false; 55 | private volatile boolean shutdownHookExecuted = false; 56 | 57 | @SuppressWarnings("squid:S106") // CLI usage intended 58 | public static void main(String[] args) { 59 | //just post the info to the console 60 | if (args.length > 0 && 61 | (args[0].equalsIgnoreCase("-v") 62 | || args[0].equalsIgnoreCase("--version") 63 | || args[0].equalsIgnoreCase("-version"))) { 64 | System.out.println("Version flag detected. Printing version info, then exiting."); 65 | System.out.println(getVersionInfo()); 66 | System.out.println("Version info printed, exiting."); 67 | return; 68 | } 69 | 70 | System.setProperty("spring.config.name", "baymax"); 71 | SpringApplication app = new SpringApplication(Launcher.class); 72 | app.setAdditionalProfiles("secrets"); 73 | 74 | app.addListeners( 75 | event -> { 76 | if (event instanceof ApplicationEnvironmentPreparedEvent) { 77 | log.info(getVersionInfo()); 78 | } 79 | }, 80 | event -> { 81 | if (event instanceof ApplicationFailedEvent failed) { 82 | log.error("Application failed", failed.getException()); 83 | } 84 | } 85 | ); 86 | app.run(args); 87 | } 88 | 89 | public Launcher(ObjectProvider shardManager, ScheduledThreadPoolExecutor jdaThreadPool) { 90 | this.shutdownHook = new Thread(() -> { 91 | try { 92 | shutdown(shardManager, jdaThreadPool); 93 | } catch (Exception e) { 94 | log.error("Uncaught exception in shutdown hook", e); 95 | } finally { 96 | this.shutdownHookExecuted = true; 97 | } 98 | }, "shutdown-hook"); 99 | } 100 | 101 | @Override 102 | public void run(ApplicationArguments args) { 103 | Runtime.getRuntime().addShutdownHook(this.shutdownHook); 104 | this.shutdownHookAdded = true; 105 | } 106 | 107 | @PreDestroy 108 | public void waitOnShutdownHook() { 109 | 110 | // This condition can happen when spring encountered an exception during start up and is tearing itself down, 111 | // but did not call System.exit, so out shutdown hooks are not being executed. 112 | // If spring is tearing itself down, we always want to exit the JVM, so we call System.exit manually here, so 113 | // our shutdown hooks will be run, and the loop below does not hang forever. 114 | if (!isShuttingDown()) { 115 | System.exit(1); 116 | } 117 | 118 | while (this.shutdownHookAdded && !this.shutdownHookExecuted) { 119 | log.info("Waiting on main shutdown hook to be done..."); 120 | try { 121 | Thread.sleep(5000); 122 | } catch (InterruptedException ignored) { 123 | Thread.currentThread().interrupt(); 124 | } 125 | } 126 | 127 | log.info("Main shutdown hook done! Proceeding."); 128 | } 129 | 130 | private static final Thread DUMMY_HOOK = new Thread(); 131 | 132 | public static boolean isShuttingDown() { 133 | try { 134 | Runtime.getRuntime().addShutdownHook(DUMMY_HOOK); 135 | Runtime.getRuntime().removeShutdownHook(DUMMY_HOOK); 136 | } catch (IllegalStateException ignored) { 137 | return true; 138 | } 139 | return false; 140 | } 141 | 142 | private void shutdown(ObjectProvider shardManager, ScheduledThreadPoolExecutor jdaThreadPool) { 143 | //okHttpClient claims that a shutdown isn't necessary 144 | 145 | //shutdown JDA 146 | log.info("Shutting down shards"); 147 | Optional.ofNullable(shardManager.getIfAvailable()).ifPresent(ShardManager::shutdown); 148 | 149 | //shutdown executors 150 | log.info("Shutting down jda thread pool"); 151 | final List jdaThreadPoolRunnables = jdaThreadPool.shutdownNow(); 152 | log.info("{} jda thread pool runnables cancelled", jdaThreadPoolRunnables.size()); 153 | 154 | try { 155 | jdaThreadPool.awaitTermination(30, TimeUnit.SECONDS); 156 | log.info("Jda thread pool terminated"); 157 | } catch (final InterruptedException e) { 158 | log.warn("Interrupted while awaiting executors termination", e); 159 | Thread.currentThread().interrupt(); 160 | } 161 | } 162 | 163 | private static String getVersionInfo() { 164 | // deduplication of individual lines doesnt make any sense for ascii art 165 | @SuppressWarnings("squid:S1192") 166 | String baymax 167 | //copypasta'd from http://textart4u.blogspot.com/2014/10/disney-baymax-face-text-art-copy-paste.html 168 | = "\t¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶\n" 169 | + "\t¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶\n" 170 | + "\t¶¶¶¶¶¶¶¶¶___________________¶¶¶¶¶¶¶¶¶\n" 171 | + "\t¶¶¶¶¶¶_________________________¶¶¶¶¶¶\n" 172 | + "\t¶¶¶¶_____________________________¶¶¶¶\n" 173 | + "\t¶¶¶______¶¶¶_____________¶¶¶______¶¶¶\n" 174 | + "\t¶¶______¶¶¶¶¶___________¶¶¶¶¶______¶¶\n" 175 | + "\t¶¶______¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶______¶¶\n" 176 | + "\t¶¶_______¶¶¶_____________¶¶¶_______¶¶\n" 177 | + "\t¶¶¶_______________________________¶¶¶\n" 178 | + "\t¶¶¶¶_____________________________¶¶¶¶\n" 179 | + "\t¶¶¶¶¶¶_________________________¶¶¶¶¶¶\n" 180 | + "\t¶¶¶¶¶¶¶¶_____________________¶¶¶¶¶¶¶¶\n" 181 | + "\t¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶\n" 182 | + "\t¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶\n"; 183 | 184 | return "\n\n" + baymax 185 | + "\n" 186 | + "\n\tVersion: " + AppInfo.getAppInfo().getVersion() 187 | + "\n\tBuild: " + AppInfo.getAppInfo().getBuildNumber() 188 | + "\n\tBuild time: " + asTimeInCentralEurope(AppInfo.getAppInfo().getBuildTime()) 189 | + "\n\tCommit: " + GitRepoState.getGitRepositoryState().commitIdAbbrev + " (" + GitRepoState.getGitRepositoryState().branch + ")" 190 | + "\n\tCommit time: " + asTimeInCentralEurope(GitRepoState.getGitRepositoryState().commitTime * 1000) 191 | + "\n\tJVM: " + System.getProperty("java.version") 192 | + "\n\tJDA: " + JDAInfo.VERSION 193 | + "\n"; 194 | } 195 | 196 | private static String asTimeInCentralEurope(final long epochMillis) { 197 | return timeInCentralEuropeFormatter().format(Instant.ofEpochMilli(epochMillis)); 198 | } 199 | 200 | //DateTimeFormatter is not threadsafe 201 | private static DateTimeFormatter timeInCentralEuropeFormatter() { 202 | return DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss z") 203 | .withZone(ZoneId.of("Europe/Berlin")); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/ModelLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.lang.Nullable; 23 | import org.springframework.stereotype.Component; 24 | import space.npstr.baymax.helpdesk.ModelParser; 25 | import space.npstr.baymax.helpdesk.Node; 26 | import space.npstr.baymax.helpdesk.exception.MalformedModelException; 27 | 28 | import java.io.File; 29 | import java.io.FileInputStream; 30 | import java.io.IOException; 31 | import java.io.InputStream; 32 | import java.net.URI; 33 | import java.net.http.HttpClient; 34 | import java.net.http.HttpRequest; 35 | import java.net.http.HttpResponse; 36 | import java.nio.file.Files; 37 | import java.nio.file.Path; 38 | import java.util.Collections; 39 | import java.util.Map; 40 | import java.util.concurrent.ConcurrentHashMap; 41 | 42 | /** 43 | * Created by napster on 05.09.18. 44 | */ 45 | @Component 46 | public class ModelLoader { 47 | 48 | private static final Logger log = LoggerFactory.getLogger(ModelLoader.class); 49 | 50 | private final Map> models = new ConcurrentHashMap<>(); 51 | 52 | private final ModelParser modelParser = new ModelParser(); 53 | private final HttpClient httpClient = HttpClient.newHttpClient(); 54 | private final Path tempDir; 55 | 56 | public ModelLoader() throws IOException { 57 | this.tempDir = Files.createTempDirectory("baymax_models"); 58 | this.tempDir.toFile().deleteOnExit(); 59 | } 60 | 61 | public Map getModel(String name, @Nullable URI uri) { 62 | return this.models.computeIfAbsent(name, __ -> loadModel(name, uri)); 63 | } 64 | 65 | /** 66 | * @throws RuntimeException if there is a general problem loading the model 67 | * @throws MalformedModelException if there is a problem parsing the model 68 | */ 69 | public Map attemptReload(String name, @Nullable URI uri) { 70 | Map model = loadModel(name, uri); 71 | this.models.put(name, model); 72 | return model; 73 | } 74 | 75 | private Map loadModel(String name, @Nullable URI uri) { 76 | String rawModel; 77 | if (uri != null) { 78 | try { 79 | rawModel = loadModelFromUrl(name, uri); 80 | } catch (Exception e) { 81 | throw new RuntimeException("Failed to load model" + name + " from url " + uri, e); 82 | } 83 | } else { 84 | String filePath = "models/" + name + ".yaml"; 85 | rawModel = loadModelAsYamlString(filePath); 86 | } 87 | 88 | return Collections.unmodifiableMap( 89 | this.modelParser.parse(rawModel) 90 | ); 91 | } 92 | 93 | public String loadModelFromUrl(String name, URI uri) throws IOException, InterruptedException { 94 | Path tempFile = Files.createTempFile(tempDir, name, ".yaml"); 95 | tempFile.toFile().deleteOnExit(); 96 | 97 | HttpResponse.BodyHandler bodyHandler = HttpResponse.BodyHandlers.ofFile(tempFile); 98 | 99 | HttpRequest request = HttpRequest.newBuilder() 100 | .GET() 101 | .uri(uri) 102 | .build(); 103 | HttpResponse response = httpClient.send(request, bodyHandler); 104 | 105 | log.debug("Fetched model {} from {} with status {} and saved to {}", name, uri, response.statusCode(), tempFile); 106 | 107 | return loadModelAsYamlString(tempFile.toFile()); 108 | } 109 | 110 | private String loadModelAsYamlString(String fileName) { 111 | return loadModelAsYamlString(new File(fileName)); 112 | } 113 | 114 | private String loadModelAsYamlString(File modelFile) { 115 | if (!modelFile.exists() || !modelFile.canRead()) { 116 | throw new RuntimeException("Failed to find or read model file " + modelFile.getName()); 117 | } 118 | try (InputStream fileStream = new FileInputStream(modelFile)) { 119 | return new String(fileStream.readAllBytes()); 120 | } catch (IOException e) { 121 | throw new RuntimeException("Failed to load model " + modelFile.getName(), e); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/NodeContext.java: -------------------------------------------------------------------------------- 1 | package space.npstr.baymax; 2 | 3 | import space.npstr.baymax.helpdesk.Node; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * Provide some context around a node, for example the node that was previously visited. 9 | */ 10 | public class NodeContext { 11 | 12 | private final Node node; 13 | private final Optional previousNodeContext; 14 | 15 | public NodeContext(Node node, Optional previousNodeContext) { 16 | this.node = node; 17 | this.previousNodeContext = previousNodeContext; 18 | } 19 | 20 | public Node getNode() { 21 | return this.node; 22 | } 23 | 24 | public Optional getPreviousNodeContext() { 25 | return this.previousNodeContext; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/RestActions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2022 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import java.time.Duration; 21 | import java.util.List; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.CompletionStage; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.function.Function; 26 | import net.dv8tion.jda.api.entities.Guild; 27 | import net.dv8tion.jda.api.entities.Member; 28 | import net.dv8tion.jda.api.entities.Message; 29 | import net.dv8tion.jda.api.entities.MessageHistory; 30 | import net.dv8tion.jda.api.entities.Role; 31 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 32 | import net.dv8tion.jda.api.exceptions.HierarchyException; 33 | import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; 34 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 35 | import org.springframework.stereotype.Component; 36 | 37 | /** 38 | * Created by napster on 21.09.18. 39 | */ 40 | @Component 41 | public class RestActions { 42 | 43 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RestActions.class); 44 | 45 | public void assignRole(Guild guild, Member member, Role role) { 46 | try { 47 | guild.addRoleToMember(member, role).queue(); 48 | } catch (InsufficientPermissionException e) { 49 | log.error("Can't assign role {} due to missing permission {}", role, e.getPermission(), e); 50 | } catch (HierarchyException e) { 51 | log.error("Can't assign role {} due to hierarchy issue", role, e); 52 | } 53 | } 54 | 55 | public void removeRole(Guild guild, Member member, Role role) { 56 | try { 57 | guild.removeRoleFromMember(member, role).queue(); 58 | } catch (InsufficientPermissionException e) { 59 | log.error("Can't remove role {} due to missing permission {}", role, e.getPermission(), e); 60 | } catch (HierarchyException e) { 61 | log.error("Can't remove role {} due to hierarchy issue", role, e); 62 | } 63 | } 64 | 65 | public CompletionStage sendMessage(MessageChannel channel, MessageCreateData message) { 66 | return channel.sendMessage(message).submit() 67 | .thenApply(Function.identity()); //avoid JDA's Promise#toCompletableFuture's UnsupportedOperationException 68 | } 69 | 70 | public CompletionStage deleteMessageAfter(Message message, Duration after) { 71 | return message.delete() 72 | .submitAfter(after.toMillis(), TimeUnit.MILLISECONDS); 73 | } 74 | 75 | public CompletionStage purgeChannel(MessageChannel channel) { 76 | return fetchAllMessages(channel.getHistory()) 77 | .thenApply(channel::purgeMessages) 78 | .thenCompose(requestFutures -> CompletableFuture.allOf(requestFutures.toArray(new CompletableFuture[0]))); 79 | } 80 | 81 | private CompletionStage> fetchAllMessages(MessageHistory history) { 82 | return history.retrievePast(100).submit() 83 | .thenApply(Function.identity()) //avoid JDA's Promise#toCompletableFuture's UnsupportedOperationException 84 | .thenCompose( 85 | messages -> { 86 | if (!messages.isEmpty()) { 87 | return fetchAllMessages(history); 88 | } 89 | return CompletableFuture.completedStage(history.getRetrievedHistory()); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/UserDialogue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Objects; 24 | import java.util.Optional; 25 | import java.util.concurrent.CompletableFuture; 26 | import java.util.concurrent.TimeUnit; 27 | import java.util.concurrent.atomic.AtomicReference; 28 | import java.util.function.Predicate; 29 | import java.util.stream.Collectors; 30 | import net.dv8tion.jda.api.entities.Guild; 31 | import net.dv8tion.jda.api.entities.Member; 32 | import net.dv8tion.jda.api.entities.Role; 33 | import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 34 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 35 | import net.dv8tion.jda.api.sharding.ShardManager; 36 | import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; 37 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 38 | import space.npstr.baymax.db.TemporaryRoleService; 39 | import space.npstr.baymax.helpdesk.Branch; 40 | import space.npstr.baymax.helpdesk.Node; 41 | 42 | /** 43 | * Created by napster on 05.09.18. 44 | */ 45 | public class UserDialogue { 46 | 47 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserDialogue.class); 48 | 49 | private final EventWaiter eventWaiter; 50 | private final ShardManager shardManager; 51 | private final Map model; 52 | private final long userId; 53 | private final long channelId; 54 | private final RestActions restActions; 55 | private final TemporaryRoleService temporaryRoleService; 56 | private final EmojisNumbersParser emojisNumbersParser = new EmojisNumbersParser(); 57 | private final List messagesToCleanUp = new ArrayList<>(); 58 | private final AtomicReference> waitingEvent = new AtomicReference<>(); 59 | private boolean done = false; 60 | 61 | public UserDialogue(EventWaiter eventWaiter, Map model, MessageReceivedEvent event, 62 | RestActions restActions, TemporaryRoleService temporaryRoleService) { 63 | 64 | this.eventWaiter = eventWaiter; 65 | this.shardManager = Objects.requireNonNull(event.getJDA().getShardManager(), "Shard Manager required"); 66 | this.model = model; 67 | this.userId = event.getAuthor().getIdLong(); 68 | this.channelId = event.getChannel().getIdLong(); 69 | this.restActions = restActions; 70 | this.temporaryRoleService = temporaryRoleService; 71 | 72 | this.messagesToCleanUp.add(event.getMessageIdLong()); 73 | 74 | NodeContext nodeContext = new NodeContext(model.get("root"), Optional.empty()); 75 | parseUserInput(event, nodeContext); 76 | } 77 | 78 | public synchronized void done() { 79 | var we = this.waitingEvent.get(); 80 | if (we != null) { 81 | we.cancel(); 82 | } 83 | 84 | if (this.done) { 85 | return; 86 | } 87 | this.done = true; 88 | 89 | getTextChannel().ifPresent(textChannel -> { 90 | List messageIdsAsStrings = this.messagesToCleanUp.stream() 91 | .map(Number::toString) 92 | .collect(Collectors.toList()); 93 | List> requestFutures = textChannel.purgeMessagesById(messageIdsAsStrings); 94 | requestFutures.forEach(f -> f.whenComplete((__, t) -> { 95 | if (t != null) { 96 | log.error("Failed to purge messages for user {} in channel {}", this.userId, this.channelId, t); 97 | } 98 | })); 99 | }); 100 | } 101 | 102 | private Optional getTextChannel() { 103 | return Optional.ofNullable(this.shardManager.getTextChannelById(this.channelId)); 104 | } 105 | 106 | private void assignRole(TextChannel textChannel, long roleId) { 107 | Guild guild = textChannel.getGuild(); 108 | Role role = guild.getRoleById(roleId); 109 | if (role == null) { 110 | log.warn("Where did the role {} go?", roleId); 111 | return; 112 | } 113 | 114 | Member member = guild.getMemberById(this.userId); 115 | if (member == null) { 116 | log.warn("No member found for user {}", this.userId); 117 | return; 118 | } 119 | 120 | this.restActions.assignRole(guild, member, role); 121 | this.temporaryRoleService.setTemporaryRole(member.getUser(), role); 122 | } 123 | 124 | private void sendNode(NodeContext nodeContext) { 125 | Optional textChannelOpt = getTextChannel(); 126 | if (textChannelOpt.isPresent()) { 127 | TextChannel textChannel = textChannelOpt.get(); 128 | this.restActions.sendMessage(textChannel, asMessage(nodeContext)) 129 | .thenAccept(message -> this.messagesToCleanUp.add(message.getIdLong())) 130 | .whenComplete((__, t) -> { 131 | if (t != null) { 132 | log.error("Failed to send message", t); 133 | } 134 | }); 135 | 136 | Optional.ofNullable(nodeContext.getNode().getRoleId()).ifPresent(roleId -> assignRole(textChannel, roleId)); 137 | } else { 138 | log.warn("Where did the channel {} go?", this.channelId); 139 | } 140 | 141 | this.waitingEvent.set(this.eventWaiter.waitForEvent( 142 | MessageReceivedEvent.class, 143 | messageOfThisUser(), 144 | event -> this.parseUserInput(event, nodeContext), 145 | HelpDeskListener.EXPIRE_MINUTES, TimeUnit.MINUTES, 146 | this::done 147 | )); 148 | } 149 | 150 | private void parseUserInput(MessageReceivedEvent event, NodeContext currentNodeContext) { 151 | this.messagesToCleanUp.add(event.getMessageIdLong()); 152 | String contentRaw = event.getMessage().getContentRaw(); 153 | Node currentNode = currentNodeContext.getNode(); 154 | Optional previousNodeContext = currentNodeContext.getPreviousNodeContext(); 155 | 156 | int numberPicked; 157 | try { 158 | numberPicked = Integer.parseInt(contentRaw); 159 | } catch (NumberFormatException e) { 160 | Optional numberOpt = this.emojisNumbersParser.emojisToNumber(contentRaw); 161 | if (numberOpt.isEmpty()) { 162 | sendNode(currentNodeContext); //todo better message? 163 | return; 164 | } 165 | numberPicked = numberOpt.get(); 166 | } 167 | 168 | numberPicked--; //correct for shown index starting at 1 instead of 0 169 | 170 | int goBack = -1; //actually user entered 0 171 | 172 | if (numberPicked < goBack || numberPicked >= currentNode.getBranches().size()) { 173 | sendNode(currentNodeContext); //todo better message? 174 | return; 175 | } 176 | 177 | NodeContext nextNodeContext; 178 | if (numberPicked == goBack) { 179 | if (previousNodeContext.isPresent()) { 180 | nextNodeContext = previousNodeContext.get(); 181 | } else { 182 | Node rootNode = this.model.get("root"); 183 | nextNodeContext = new NodeContext(rootNode, Optional.empty()); 184 | } 185 | } else { 186 | Branch branch = currentNode.getBranches().get(numberPicked); 187 | Node nextNode = this.model.get(branch.getTargetId()); 188 | nextNodeContext = new NodeContext(nextNode, Optional.of(currentNodeContext)); 189 | } 190 | sendNode(nextNodeContext); 191 | } 192 | 193 | private Predicate messageOfThisUser() { 194 | return event -> 195 | event.getAuthor().getIdLong() == this.userId 196 | && event.getChannel().getIdLong() == this.channelId; 197 | } 198 | 199 | public static MessageCreateData asMessage(NodeContext nodeContext) { 200 | MessageCreateBuilder mb = new MessageCreateBuilder(); 201 | EmojisNumbersParser emojisNumbersParser = new EmojisNumbersParser(); 202 | Node node = nodeContext.getNode(); 203 | 204 | mb.addContent("**").addContent(node.getTitle()).addContent("**\n\n"); 205 | int bb = 1; 206 | for (Branch branch : node.getBranches()) { 207 | mb 208 | .addContent(emojisNumbersParser.numberAsEmojis(bb++)) 209 | .addContent(" ") 210 | .addContent(branch.getMessage()) 211 | .addContent("\n"); 212 | } 213 | mb.addContent("\n"); 214 | if ("root".equals(node.getId())) { 215 | mb.addContent("Say a number to start.\n"); 216 | } else { 217 | if (nodeContext.getPreviousNodeContext().isPresent()) { 218 | mb.addContent(emojisNumbersParser.numberAsEmojis(0)).addContent(" Go back.\n"); 219 | } 220 | } 221 | 222 | return mb.build(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/config/OkHttpConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.config; 19 | 20 | import okhttp3.OkHttpClient; 21 | import org.springframework.beans.factory.config.ConfigurableBeanFactory; 22 | import org.springframework.context.annotation.Bean; 23 | import org.springframework.context.annotation.Configuration; 24 | import org.springframework.context.annotation.Scope; 25 | 26 | import java.util.concurrent.TimeUnit; 27 | 28 | /** 29 | * Created by napster on 05.09.18. 30 | */ 31 | @Configuration 32 | public class OkHttpConfiguration { 33 | 34 | //a general purpose http client builder 35 | @Bean 36 | @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //do not reuse the builders 37 | public static OkHttpClient.Builder httpClientBuilder() { 38 | return new OkHttpClient.Builder() 39 | .connectTimeout(30, TimeUnit.SECONDS) 40 | .writeTimeout(30, TimeUnit.SECONDS) 41 | .readTimeout(30, TimeUnit.SECONDS) 42 | .retryOnConnectionFailure(true); 43 | } 44 | 45 | // default http client that can be used for anything 46 | @Bean 47 | public OkHttpClient defaultHttpClient(OkHttpClient.Builder httpClientBuilder) { 48 | return httpClientBuilder.build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/config/ShardManagerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.config; 19 | 20 | import net.dv8tion.jda.api.entities.Activity; 21 | import net.dv8tion.jda.api.requests.GatewayIntent; 22 | import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder; 23 | import net.dv8tion.jda.api.sharding.ShardManager; 24 | import net.dv8tion.jda.api.utils.ChunkingFilter; 25 | import net.dv8tion.jda.api.utils.MemberCachePolicy; 26 | import net.dv8tion.jda.api.utils.cache.CacheFlag; 27 | import okhttp3.OkHttpClient; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.util.ObjectUtils; 31 | import space.npstr.baymax.EventWaiter; 32 | import space.npstr.baymax.HelpDeskListener; 33 | import space.npstr.baymax.config.properties.BaymaxConfig; 34 | 35 | import java.util.EnumSet; 36 | import java.util.concurrent.ScheduledThreadPoolExecutor; 37 | import java.util.concurrent.atomic.AtomicInteger; 38 | 39 | /** 40 | * Created by napster on 05.09.18. 41 | */ 42 | @Configuration 43 | public class ShardManagerConfiguration { 44 | 45 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ShardManagerConfiguration.class); 46 | 47 | private static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER 48 | = (thread, throwable) -> log.error("Uncaught exception in thread {}", thread.getName(), throwable); 49 | 50 | @Bean 51 | public ScheduledThreadPoolExecutor jdaThreadPool() { 52 | AtomicInteger threadNumber = new AtomicInteger(0); 53 | return new ScheduledThreadPoolExecutor(50, r -> { 54 | Thread thread = new Thread(r, "jda-pool-t" + threadNumber.getAndIncrement()); 55 | thread.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER); 56 | return thread; 57 | }); 58 | } 59 | 60 | @Bean(destroyMethod = "") //we manage the lifecycle ourselves tyvm, see shutdown hook in the launcher 61 | public ShardManager shardManager(BaymaxConfig baymaxConfig, OkHttpClient.Builder httpClientBuilder, 62 | ScheduledThreadPoolExecutor jdaThreadPool, EventWaiter eventWaiter, 63 | HelpDeskListener helpDeskListener) { 64 | 65 | DefaultShardManagerBuilder shardBuilder = DefaultShardManagerBuilder 66 | .createDefault(baymaxConfig.discordToken()) 67 | .setChunkingFilter(ChunkingFilter.ALL) //we need to fetch members from the cache at several places 68 | .setMemberCachePolicy(MemberCachePolicy.ALL) 69 | .enableIntents( 70 | GatewayIntent.GUILD_MEMBERS, //required for chunking 71 | GatewayIntent.MESSAGE_CONTENT // parsing numbers 72 | ) 73 | .addEventListeners(eventWaiter) 74 | .addEventListeners(helpDeskListener) 75 | .setHttpClientBuilder(httpClientBuilder 76 | .retryOnConnectionFailure(false)) 77 | .setEnableShutdownHook(false) 78 | .setRateLimitScheduler(jdaThreadPool, false) 79 | .setRateLimitElastic(jdaThreadPool, false) 80 | .setCallbackPool(jdaThreadPool, false) 81 | .disableCache(EnumSet.allOf(CacheFlag.class)); 82 | 83 | String statusMessage = baymaxConfig.statusMessage(); 84 | if (!ObjectUtils.isEmpty(statusMessage)) { 85 | Activity.ActivityType activityType = Activity.ActivityType.fromKey(baymaxConfig.statusType()); 86 | Activity discordStatus = Activity.of(activityType, statusMessage); 87 | shardBuilder.setActivity(discordStatus); 88 | } 89 | 90 | return shardBuilder.build(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/config/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.config; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/config/properties/BaymaxConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.config.properties; 19 | 20 | import java.net.URI; 21 | import java.util.List; 22 | import java.util.Set; 23 | import org.springframework.boot.context.properties.ConfigurationProperties; 24 | import org.springframework.lang.Nullable; 25 | 26 | /** 27 | * Created by napster on 05.09.18. 28 | */ 29 | @ConfigurationProperties("baymax") 30 | public record BaymaxConfig( 31 | String discordToken, 32 | int statusType, 33 | @Nullable String statusMessage, 34 | Set staffRoleIds, 35 | List helpDesks 36 | ) { 37 | 38 | @SuppressWarnings("ConstantValue") 39 | public BaymaxConfig { 40 | if (discordToken == null || discordToken.isBlank()) { 41 | throw new IllegalArgumentException("Discord token must not be blank"); 42 | } 43 | if (staffRoleIds == null) { 44 | staffRoleIds = Set.of(); 45 | } 46 | if (helpDesks == null) { 47 | helpDesks = List.of(); 48 | } 49 | } 50 | 51 | public record HelpDesk( 52 | long channelId, 53 | String modelName, 54 | @Nullable URI modelUri 55 | ) { 56 | 57 | @SuppressWarnings("ConstantValue") 58 | public HelpDesk { 59 | if (channelId <= 0) { 60 | throw new IllegalArgumentException("Channel id must be positive"); 61 | } 62 | if (modelName == null || modelName.isBlank()) { 63 | throw new IllegalArgumentException("Model name must not be blank"); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/config/properties/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.config.properties; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/db/Database.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.db; 19 | 20 | import org.flywaydb.core.Flyway; 21 | import org.flywaydb.core.api.MigrationVersion; 22 | import org.springframework.stereotype.Component; 23 | import org.sqlite.SQLiteConfig; 24 | import org.sqlite.SQLiteDataSource; 25 | 26 | import javax.sql.DataSource; 27 | import java.sql.Connection; 28 | import java.sql.SQLException; 29 | 30 | /** 31 | * Created by napster on 21.09.18. 32 | */ 33 | @Component 34 | public class Database { 35 | 36 | private static final String JDBC_URL = "jdbc:sqlite:baymax.sqlite"; 37 | 38 | private final DataSource dataSource; 39 | 40 | public Database() { 41 | SQLiteConfig sqliteConfig = new SQLiteConfig(); 42 | SQLiteDataSource sqliteDataSource = new SQLiteDataSource(sqliteConfig); 43 | sqliteDataSource.setUrl(JDBC_URL); 44 | migrate(sqliteDataSource); 45 | 46 | this.dataSource = sqliteDataSource; 47 | } 48 | 49 | public Connection getConnection() throws SQLException { 50 | return this.dataSource.getConnection(); 51 | } 52 | 53 | private void migrate(DataSource dataSource) { 54 | Flyway flyway = new Flyway(Flyway.configure() 55 | .baselineOnMigrate(true) 56 | .baselineVersion(MigrationVersion.fromVersion("0")) 57 | .baselineDescription("Base Migration") 58 | .locations("db/migrations") 59 | .dataSource(dataSource) 60 | ); 61 | flyway.migrate(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/db/TemporaryRoleService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.db; 19 | 20 | import net.dv8tion.jda.api.JDA; 21 | import net.dv8tion.jda.api.entities.Guild; 22 | import net.dv8tion.jda.api.entities.Member; 23 | import net.dv8tion.jda.api.entities.Role; 24 | import net.dv8tion.jda.api.entities.User; 25 | import net.dv8tion.jda.api.sharding.ShardManager; 26 | import org.springframework.beans.factory.ObjectProvider; 27 | import org.springframework.stereotype.Component; 28 | import space.npstr.baymax.RestActions; 29 | 30 | import java.sql.Connection; 31 | import java.sql.PreparedStatement; 32 | import java.sql.ResultSet; 33 | import java.sql.SQLException; 34 | import java.sql.Statement; 35 | import java.time.OffsetDateTime; 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | import java.util.concurrent.Executors; 39 | import java.util.concurrent.TimeUnit; 40 | 41 | /** 42 | * Created by napster on 21.09.18. 43 | */ 44 | @Component 45 | public class TemporaryRoleService { 46 | 47 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TemporaryRoleService.class); 48 | 49 | private static final int KEEP_ROLE_FOR_HOURS = 3; 50 | 51 | private final Database database; 52 | private final ObjectProvider shardManager; 53 | private final RestActions restActions; 54 | 55 | public TemporaryRoleService(Database database, ObjectProvider shardManager, RestActions restActions) { 56 | this.database = database; 57 | this.shardManager = shardManager; 58 | this.restActions = restActions; 59 | var cleaner = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "temporary-role-service")); 60 | 61 | cleaner.scheduleAtFixedRate(this::cleanUp, 1, 1, TimeUnit.MINUTES); 62 | } 63 | 64 | public void setTemporaryRole(User user, Role role) { 65 | long userId = user.getIdLong(); 66 | long roleId = role.getIdLong(); 67 | long guildId = role.getGuild().getIdLong(); 68 | long until = OffsetDateTime.now() 69 | .plusHours(KEEP_ROLE_FOR_HOURS) 70 | .toInstant().toEpochMilli(); 71 | 72 | //language=SQLite 73 | String existsSql = "SELECT EXISTS(SELECT * FROM temporary_role WHERE user_id = ? AND role_id = ?);"; 74 | 75 | //language=SQLite 76 | String insertSql = "INSERT INTO temporary_role VALUES(?, ?, ?, ?);"; 77 | //language=SQLite 78 | String updateSql = "UPDATE temporary_role SET guild_id = ?, until = ? WHERE user_id = ? AND role_id = ?;"; 79 | 80 | try (Connection connection = this.database.getConnection()) { 81 | boolean exists; 82 | try (PreparedStatement statement = connection.prepareStatement(existsSql)) { 83 | statement.setLong(1, userId); 84 | statement.setLong(2, roleId); 85 | try (ResultSet resultSet = statement.executeQuery()) { 86 | exists = resultSet.getBoolean(1); 87 | } 88 | 89 | } 90 | String query = exists ? updateSql : insertSql; 91 | 92 | try (PreparedStatement statement = connection.prepareStatement(query)) { 93 | if (exists) { 94 | statement.setLong(1, guildId); 95 | statement.setLong(2, until); 96 | statement.setLong(3, userId); 97 | statement.setLong(4, roleId); 98 | } else { 99 | statement.setLong(1, userId); 100 | statement.setLong(2, roleId); 101 | statement.setLong(3, guildId); 102 | statement.setLong(4, until); 103 | } 104 | statement.execute(); 105 | } 106 | } catch (SQLException e) { 107 | log.error("Failed to set temporary role. User: {} Role: {}", user, role, e); 108 | } 109 | } 110 | 111 | @SuppressWarnings("squid:S135") // the while loop is perfectly readable 112 | private void cleanUp() { 113 | //avoid runnign this when were not ready 114 | if (this.shardManager.getObject().getShardCache().stream() 115 | .anyMatch(shard -> shard.getStatus() != JDA.Status.CONNECTED)) { 116 | return; 117 | } 118 | 119 | //language=SQLite 120 | String query = "SELECT * FROM temporary_role;"; 121 | 122 | try (Connection connection = this.database.getConnection()) { 123 | List toDelete = new ArrayList<>(); 124 | try (Statement statement = connection.createStatement()) { 125 | try (ResultSet resultSet = statement.executeQuery(query)) { 126 | while (resultSet.next()) { 127 | long until = resultSet.getLong(4); 128 | if (System.currentTimeMillis() < until) { 129 | continue; 130 | } 131 | long userId = resultSet.getLong(1); 132 | long roleId = resultSet.getLong(2); 133 | long guildId = resultSet.getLong(3); 134 | Guild guild = this.shardManager.getObject().getGuildById(guildId); 135 | if (guild == null) { //we left the guild. dont do anything, we might get readded 136 | continue; 137 | } 138 | Role role = guild.getRoleById(roleId); 139 | Member member = guild.getMemberById(userId); 140 | if (role == null || member == null) { //role or member is gone. no point in keeping records on it 141 | toDelete.add(new TemporaryRoleId(userId, roleId)); 142 | continue; 143 | } 144 | 145 | log.debug("Removing role {} from member {}", role, member); 146 | this.restActions.removeRole(guild, member, role); 147 | 148 | //remove the row 149 | toDelete.add(new TemporaryRoleId(userId, roleId)); 150 | } 151 | } 152 | } 153 | //language=SQLite 154 | String deleteQuery = "DELETE FROM temporary_role WHERE user_id = ? AND role_id = ?;"; 155 | try (PreparedStatement statement = connection.prepareStatement(deleteQuery)) { 156 | for (TemporaryRoleId id : toDelete) { 157 | log.debug("Deleting temporary role entry for user {} and role {}", id.userId, id.roleId); 158 | statement.setLong(1, id.userId); 159 | statement.setLong(2, id.roleId); 160 | statement.execute(); 161 | } 162 | } 163 | } catch (SQLException e) { 164 | log.error("Failed to clean up", e); 165 | } 166 | } 167 | 168 | //todo clean guild members of the roles? 169 | 170 | private static class TemporaryRoleId { 171 | private final long userId; 172 | private final long roleId; 173 | 174 | public TemporaryRoleId(long userId, long roleId) { 175 | this.userId = userId; 176 | this.roleId = roleId; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/db/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.db; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/Branch.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | /** 21 | * Created by napster on 05.09.18. 22 | */ 23 | public interface Branch { 24 | 25 | /** 26 | * @return The message describing this branch. 27 | */ 28 | String getMessage(); 29 | 30 | /** 31 | * @return The id of the node which this branch is pointing at. 32 | */ 33 | String getTargetId(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/BranchModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | /** 21 | * Created by napster on 05.09.18. 22 | */ 23 | public class BranchModel implements Branch { 24 | 25 | private String message = ""; 26 | 27 | private String targetId = ""; 28 | 29 | @Override 30 | public String getMessage() { 31 | return this.message; 32 | } 33 | 34 | public void setMessage(String message) { 35 | this.message = message; 36 | } 37 | 38 | @Override 39 | public String getTargetId() { 40 | return this.targetId; 41 | } 42 | 43 | public void setTargetId(String targetId) { 44 | this.targetId = targetId; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/ModelParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | import org.yaml.snakeyaml.LoaderOptions; 21 | import org.yaml.snakeyaml.Yaml; 22 | import org.yaml.snakeyaml.constructor.Constructor; 23 | import space.npstr.baymax.helpdesk.exception.MissingTargetNodeException; 24 | import space.npstr.baymax.helpdesk.exception.NoRootNodeException; 25 | import space.npstr.baymax.helpdesk.exception.UnreferencedNodesException; 26 | 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | /** 32 | * Created by napster on 05.09.18. 33 | */ 34 | public class ModelParser { 35 | 36 | public Map parse(String yaml) { 37 | Yaml snakeYaml = new Yaml(new Constructor(NodeModel.class, new LoaderOptions())); 38 | Iterable objects = snakeYaml.loadAll(yaml); 39 | Map model = new HashMap<>(); 40 | objects.forEach(o -> model.put(((Node) o).getId(), (Node) o)); 41 | 42 | sanityCheck(model); 43 | 44 | return model; 45 | } 46 | 47 | private void sanityCheck(Map model) { 48 | // A model is considered sane when: 49 | // 0) There is a root node 50 | // 1) Each node referenced by a branch exists 51 | // 2) All nodes are referenced from the root node 52 | 53 | Map allNodes = Map.copyOf(model); 54 | Map unreferencedNodes = new HashMap<>(model); 55 | 56 | Node rootNode = model.get("root"); 57 | if (rootNode == null) { 58 | throw new NoRootNodeException(); 59 | } 60 | 61 | traverse(rootNode, allNodes, unreferencedNodes); 62 | 63 | if (!unreferencedNodes.isEmpty()) { 64 | throw new UnreferencedNodesException(unreferencedNodes.values()); 65 | } 66 | } 67 | 68 | 69 | private void traverse(Node node, Map allNodes, Map unreferencedNodes) { 70 | List branches = node.getBranches(); 71 | 72 | Node remove = unreferencedNodes.remove(node.getId()); 73 | if (remove == null) { 74 | return; //already visited this node 75 | } 76 | 77 | for (Branch branch : branches) { 78 | String targetId = branch.getTargetId(); 79 | Node targetNode = allNodes.get(targetId); 80 | if (targetNode == null) { 81 | throw new MissingTargetNodeException(node, targetId); 82 | } 83 | if ("root".equals(targetId)) { 84 | continue; 85 | } 86 | traverse(targetNode, allNodes, unreferencedNodes); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/Node.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | import java.util.List; 21 | import org.springframework.lang.Nullable; 22 | 23 | /** 24 | * Created by napster on 05.09.18. 25 | */ 26 | public interface Node { 27 | 28 | /** 29 | * @return The id of this node 30 | */ 31 | String getId(); 32 | 33 | /** 34 | * @return The title of this node. 35 | */ 36 | String getTitle(); 37 | 38 | /** 39 | * @return A possible role associated with reaching this node that will be assigned to the user. 40 | *

41 | * NOTE: We can't use Optional due to SnakeYaml aiming for Java 6 compatibility :clap: 42 | */ 43 | @Nullable 44 | Long getRoleId(); 45 | 46 | /** 47 | * @return A list of branches connecting this node to 0-n other nodes in a unidirectional way 48 | */ 49 | List getBranches(); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/NodeModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | import java.util.Collections; 21 | import java.util.List; 22 | import org.springframework.lang.Nullable; 23 | 24 | /** 25 | * Created by napster on 05.09.18. 26 | */ 27 | public class NodeModel implements Node { 28 | 29 | private String id = ""; 30 | 31 | private String title = ""; 32 | 33 | @Nullable 34 | private Long roleId = null; 35 | 36 | private List branches = Collections.emptyList(); 37 | 38 | @Override 39 | public String getId() { 40 | return this.id; 41 | } 42 | 43 | public void setId(String id) { 44 | this.id = id; 45 | } 46 | 47 | @Override 48 | public String getTitle() { 49 | return this.title; 50 | } 51 | 52 | public void setTitle(String title) { 53 | this.title = title; 54 | } 55 | 56 | @Override 57 | @Nullable 58 | public Long getRoleId() { 59 | return this.roleId; 60 | } 61 | 62 | public void setRoleId(Long roleId) { 63 | this.roleId = roleId; 64 | } 65 | 66 | @Override 67 | public List getBranches() { 68 | return this.branches; 69 | } 70 | 71 | public void setBranches(List branches) { 72 | this.branches = branches; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/exception/MalformedModelException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk.exception; 19 | 20 | /** 21 | * Created by napster on 05.09.18. 22 | */ 23 | public abstract class MalformedModelException extends RuntimeException { 24 | 25 | public MalformedModelException() { 26 | super(); 27 | } 28 | 29 | @Override 30 | public abstract String getMessage(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/exception/MissingTargetNodeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk.exception; 19 | 20 | import space.npstr.baymax.helpdesk.Node; 21 | 22 | /** 23 | * Created by napster on 05.09.18. 24 | */ 25 | public class MissingTargetNodeException extends MalformedModelException { 26 | 27 | private final Node sourceNode; 28 | private final String targetNodeId; 29 | 30 | public MissingTargetNodeException(Node sourceNode, String targetNodeId) { 31 | super(); 32 | this.sourceNode = sourceNode; 33 | this.targetNodeId = targetNodeId; 34 | } 35 | 36 | @Override 37 | public String getMessage() { 38 | return "Node " + this.sourceNode.getId() + " has a branch referencing a non-existent node " + this.targetNodeId; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/exception/NoRootNodeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk.exception; 19 | 20 | /** 21 | * Created by napster on 05.09.18. 22 | */ 23 | public class NoRootNodeException extends MalformedModelException { 24 | 25 | public NoRootNodeException() { 26 | super(); 27 | } 28 | 29 | @Override 30 | public String getMessage() { 31 | return "No root node found"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/exception/UnreferencedNodesException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk.exception; 19 | 20 | import space.npstr.baymax.helpdesk.Node; 21 | 22 | import java.util.Collection; 23 | import java.util.stream.Collectors; 24 | 25 | /** 26 | * Created by napster on 05.09.18. 27 | */ 28 | public class UnreferencedNodesException extends MalformedModelException { 29 | 30 | private final Collection unreferencedNodes; 31 | 32 | public UnreferencedNodesException(Collection unreferencedNodes) { 33 | super(); 34 | this.unreferencedNodes = unreferencedNodes; 35 | } 36 | 37 | @Override 38 | public String getMessage() { 39 | String nodes = this.unreferencedNodes.stream().map(Node::getId).collect(Collectors.joining(", ")); 40 | return "Nodes " + nodes + " have no reference leading to them from the root node."; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/exception/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.helpdesk.exception; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/helpdesk/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.helpdesk; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/info/AppInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.info; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.util.Properties; 23 | 24 | /** 25 | * Created by napster on 05.09.18. 26 | */ 27 | public class AppInfo { 28 | 29 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AppInfo.class); 30 | 31 | public static AppInfo getAppInfo() { 32 | return AppInfoHolder.INSTANCE; 33 | } 34 | 35 | //holder pattern 36 | private static final class AppInfoHolder { 37 | private static final AppInfo INSTANCE = new AppInfo(); 38 | } 39 | 40 | private final String version; 41 | private final String groupId; 42 | private final String artifactId; 43 | private final String buildNumber; 44 | private final long buildTime; 45 | 46 | private AppInfo() { 47 | InputStream resourceAsStream = this.getClass().getResourceAsStream("/app.properties"); 48 | Properties prop = new Properties(); 49 | try { 50 | prop.load(resourceAsStream); 51 | } catch (IOException e) { 52 | log.error("Failed to load app.properties", e); 53 | } 54 | this.version = prop.getProperty("version"); 55 | this.groupId = prop.getProperty("groupId"); 56 | this.artifactId = prop.getProperty("artifactId"); 57 | this.buildNumber = prop.getProperty("buildNumber"); 58 | this.buildTime = Long.parseLong(prop.getProperty("buildTime")); 59 | } 60 | 61 | public String getVersion() { 62 | return this.version; 63 | } 64 | 65 | public String getGroupId() { 66 | return this.groupId; 67 | } 68 | 69 | public String getArtifactId() { 70 | return this.artifactId; 71 | } 72 | 73 | public String getBuildNumber() { 74 | return this.buildNumber; 75 | } 76 | 77 | public long getBuildTime() { 78 | return this.buildTime; 79 | } 80 | 81 | public String getVersionBuild() { 82 | return this.version + "_" + this.buildNumber; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/info/GitRepoState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.info; 19 | 20 | import java.io.IOException; 21 | import java.time.OffsetDateTime; 22 | import java.time.format.DateTimeFormatter; 23 | import java.util.Properties; 24 | 25 | /** 26 | * Created by napster on 05.09.18. 27 | */ 28 | public class GitRepoState { 29 | 30 | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GitRepoState.class); 31 | 32 | public static GitRepoState getGitRepositoryState() { 33 | return GitRepoStateHolder.INSTANCE; 34 | } 35 | 36 | //holder pattern 37 | private static final class GitRepoStateHolder { 38 | private static final GitRepoState INSTANCE = new GitRepoState("git.properties"); 39 | } 40 | 41 | public final String branch; 42 | public final String commitId; 43 | public final String commitIdAbbrev; 44 | public final String commitUserName; 45 | public final String commitUserEmail; 46 | public final String commitMessageFull; 47 | public final String commitMessageShort; 48 | public final long commitTime; //epoch seconds 49 | 50 | @SuppressWarnings("ConstantConditions") 51 | public GitRepoState(String propsName) { 52 | 53 | Properties properties = new Properties(); 54 | try { 55 | properties.load(GitRepoState.class.getClassLoader().getResourceAsStream(propsName)); 56 | } catch (NullPointerException | IOException e) { 57 | log.info("Failed to load git repo information", e); //need to build with build tool to get them 58 | } 59 | 60 | this.branch = String.valueOf(properties.getOrDefault("git.branch", "")); 61 | this.commitId = String.valueOf(properties.getOrDefault("git.commit.id", "")); 62 | this.commitIdAbbrev = String.valueOf(properties.getOrDefault("git.commit.id.abbrev", "")); 63 | this.commitUserName = String.valueOf(properties.getOrDefault("git.commit.user.name", "")); 64 | this.commitUserEmail = String.valueOf(properties.getOrDefault("git.commit.user.email", "")); 65 | this.commitMessageFull = String.valueOf(properties.getOrDefault("git.commit.message.full", "")); 66 | this.commitMessageShort = String.valueOf(properties.getOrDefault("git.commit.message.short", "")); 67 | final String time = String.valueOf(properties.get("git.commit.time")); 68 | if (time == null) { 69 | this.commitTime = 0; 70 | } else { 71 | this.commitTime = OffsetDateTime.from(getDtf().parse(time)).toEpochSecond(); 72 | } 73 | } 74 | 75 | //DateTimeFormatter is not threadsafe 76 | private DateTimeFormatter getDtf() { 77 | // https://github.com/n0mer/gradle-git-properties/issues/71 78 | return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/info/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax.info; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/java/space/npstr/baymax/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018-2023 the original author or authors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | @NonNullApi 19 | @NonNullFields 20 | package space.npstr.baymax; 21 | 22 | import org.springframework.lang.NonNullApi; 23 | import org.springframework.lang.NonNullFields; 24 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "baymax.discord-token", 5 | "type": "java.lang.String", 6 | "description": "Discord Bot Token." 7 | }, 8 | { 9 | "name": "sentry.dsn", 10 | "type": "java.lang.String", 11 | "description": "DSN of your sentry project." 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/main/resources/app.properties: -------------------------------------------------------------------------------- 1 | version=@project.version@ 2 | groupId=@project.groupId@ 3 | artifactId=@project.artifactId@ 4 | buildNumber=@env.BUILD_NUMBER@ 5 | buildTime=@env.BUILD_TIME@ 6 | -------------------------------------------------------------------------------- /src/main/resources/baymax.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | output: 3 | ansi: 4 | enabled: always 5 | main: 6 | banner-mode: log 7 | 8 | logging: 9 | file.name: './logs/baymax.log' 10 | file.max-history: 30 11 | file.max-size: 1GB 12 | 13 | level: 14 | root: INFO 15 | space.npstr: TRACE 16 | 17 | sentry: 18 | dsn: "" 19 | logging.enabled: false 20 | in-app-includes: 21 | - "space.npstr" 22 | - "dev.capybaralabs" 23 | logging: 24 | minimum-event-level: warn 25 | 26 | baymax: 27 | status-type: 1 28 | status-message: "" 29 | staff-role-ids: 30 | - 479601466110377984 # Staff in Aki's Lair 31 | - 340205944866865162 # Moderators in Wolfia 32 | - 321115861924446208 # Botfather in Wolfia 33 | help-desks: 34 | - channel-id: 487947091960659969 35 | model-name: aki 36 | model-uri: "https://raw.githubusercontent.com/CapybaraLabs/aki-wiki/master/baymax_model.yaml" 37 | - channel-id: 634144815709290510 38 | model-name: wolfia 39 | model-uri: "https://raw.githubusercontent.com/wolfiabot/baymax_helpdesk/master/baymax_helpdesk.yaml" 40 | -------------------------------------------------------------------------------- /src/main/resources/db/migrations/V1__AddTemporaryRole.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE temporary_role 2 | ( 3 | user_id bigint NOT NULL, 4 | role_id bigint NOT NULL, 5 | guild_id bigint NOT NULL, 6 | until bigint NOT NULL, 7 | PRIMARY KEY (user_id, role_id) 8 | ) -------------------------------------------------------------------------------- /src/test/java/space/npstr/baymax/EmojisNumbersParserTest.java: -------------------------------------------------------------------------------- 1 | package space.npstr.baymax; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Optional; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | 12 | class EmojisNumbersParserTest { 13 | 14 | private final EmojisNumbersParser parser = new EmojisNumbersParser(); 15 | 16 | @Test 17 | void fortyTwo() { 18 | String fortyTwo = this.parser.numberAsEmojis(42); 19 | assertEquals(EmojisNumbersParser.FOUR + EmojisNumbersParser.TWO, fortyTwo); 20 | 21 | Optional result = this.parser.emojisToNumber(fortyTwo); 22 | assertTrue(result.isPresent()); 23 | assertEquals(42, (int) result.get()); 24 | } 25 | 26 | @Test 27 | void maxInt() { 28 | String value = this.parser.numberAsEmojis(Integer.MAX_VALUE); 29 | 30 | Optional result = this.parser.emojisToNumber(value); 31 | assertTrue(result.isPresent()); 32 | assertEquals(Integer.MAX_VALUE, (int) result.get()); 33 | } 34 | 35 | @Test 36 | void zero() { 37 | String value = this.parser.numberAsEmojis(0); 38 | 39 | Optional result = this.parser.emojisToNumber(value); 40 | assertTrue(result.isPresent()); 41 | assertEquals(0, (int) result.get()); 42 | } 43 | 44 | @Test 45 | void negative_shouldThrow() { 46 | assertThrows(NumberFormatException.class, () -> this.parser.numberAsEmojis(-1)); 47 | } 48 | 49 | @Test 50 | void notANumberEmoji_shouldBeEmpty() { 51 | Optional poop = this.parser.emojisToNumber(Emojis.get("poop")); 52 | assertTrue(poop.isEmpty()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/space/npstr/baymax/helpdesk/ModelParserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Dennis Neufeld 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published 6 | * by the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package space.npstr.baymax.helpdesk; 19 | 20 | import org.junit.jupiter.api.Test; 21 | import space.npstr.baymax.helpdesk.exception.MissingTargetNodeException; 22 | import space.npstr.baymax.helpdesk.exception.NoRootNodeException; 23 | import space.npstr.baymax.helpdesk.exception.UnreferencedNodesException; 24 | 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | import java.util.Optional; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; 32 | import static org.junit.jupiter.api.Assertions.assertNotNull; 33 | import static org.junit.jupiter.api.Assertions.assertThrows; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | /** 37 | * Created by napster on 05.09.18. 38 | */ 39 | public class ModelParserTest { 40 | 41 | private final ModelParser modelParser = new ModelParser(); 42 | 43 | @Test 44 | void parse_SaneModel_Ok() throws IOException { 45 | Map model = this.modelParser.parse(loadModelAsYamlString("models/sane.yaml")); 46 | 47 | Node root = model.get("root"); 48 | assertNotNull(root); 49 | assertEquals("How may I help you?", root.getTitle()); 50 | assertEquals(2, root.getBranches().size()); 51 | 52 | Node supportRole = model.get("support-role"); 53 | assertNotNull(supportRole); 54 | Optional roleId = Optional.ofNullable(supportRole.getRoleId()); 55 | assertTrue(roleId.isPresent()); 56 | assertEquals(487925645989380108L, roleId.get().longValue()); 57 | } 58 | 59 | @Test 60 | void parse_ModelWithoutRootNode_ExceptionThrown() { 61 | assertThrows( 62 | NoRootNodeException.class, 63 | () -> this.modelParser.parse(loadModelAsYamlString("models/no_root_node.yaml")) 64 | ); 65 | } 66 | 67 | @Test 68 | void parse_ModelWithMissingNode_ExceptionThrown() { 69 | assertThrows( 70 | MissingTargetNodeException.class, 71 | () -> this.modelParser.parse(loadModelAsYamlString("models/missing_node.yaml")) 72 | ); 73 | } 74 | 75 | @Test 76 | void parse_ModelWithUnreferencedNode_ExceptionThrown() { 77 | assertThrows( 78 | UnreferencedNodesException.class, 79 | () -> this.modelParser.parse(loadModelAsYamlString("models/unreferenced_node.yaml")) 80 | ); 81 | } 82 | 83 | @Test 84 | void parse_ModelWithRootLoop_Ok() throws IOException { 85 | Map model = this.modelParser.parse(loadModelAsYamlString("models/root_loop.yaml")); 86 | assertNotNull(model.get("root")); 87 | assertNotNull(model.get("foo-bar")); 88 | assertEquals("root", model.get("foo-bar").getBranches().get(0).getTargetId()); 89 | } 90 | 91 | @Test 92 | void parse_ModelWithBranchLoop_Ok() throws IOException { 93 | Map model = this.modelParser.parse(loadModelAsYamlString("models/branch_loop.yaml")); 94 | assertNotNull(model.get("root")); 95 | assertNotNull(model.get("foo")); 96 | assertNotNull(model.get("bar")); 97 | assertEquals("bar", model.get("foo").getBranches().get(0).getTargetId()); 98 | assertEquals("foo", model.get("bar").getBranches().get(0).getTargetId()); 99 | } 100 | 101 | @Test 102 | void parse_ModelWithLargeBranchLoop_Ok() throws IOException { 103 | Map model = this.modelParser.parse(loadModelAsYamlString("models/branch_loop_large.yaml")); 104 | assertNotNull(model.get("root")); 105 | assertNotNull(model.get("zero")); 106 | assertNotNull(model.get("five")); 107 | assertNotNull(model.get("eight")); 108 | 109 | assertEquals("two", model.get("one").getBranches().get(0).getTargetId()); 110 | assertEquals("four", model.get("three").getBranches().get(0).getTargetId()); 111 | assertEquals("seven", model.get("six").getBranches().get(0).getTargetId()); 112 | assertEquals("zero", model.get("nine").getBranches().get(0).getTargetId()); 113 | } 114 | 115 | 116 | private String loadModelAsYamlString(String resourceName) throws IOException { 117 | InputStream fileStream = ModelParserTest.class.getClassLoader().getResourceAsStream(resourceName); 118 | return new String(Objects.requireNonNull(fileStream).readAllBytes()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/resources/models/branch_loop.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "Whatever" 4 | branches: 5 | - message: "Foo" 6 | targetId: foo 7 | 8 | --- 9 | id: foo 10 | title: "Whatever" 11 | branches: 12 | - message: "Bar" 13 | targetId: bar 14 | 15 | --- 16 | id: bar 17 | title: "Whatever" 18 | branches: 19 | - message: "Foo" 20 | targetId: foo 21 | -------------------------------------------------------------------------------- /src/test/resources/models/branch_loop_large.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "Root" 4 | branches: 5 | - message: "Zero" 6 | targetId: zero 7 | 8 | --- 9 | id: zero 10 | title: "Zero" 11 | branches: 12 | - message: "One" 13 | targetId: one 14 | 15 | --- 16 | id: one 17 | title: "One" 18 | branches: 19 | - message: "Two" 20 | targetId: two 21 | 22 | --- 23 | id: two 24 | title: "Two" 25 | branches: 26 | - message: "Three" 27 | targetId: three 28 | 29 | --- 30 | id: three 31 | title: "Three" 32 | branches: 33 | - message: "Four" 34 | targetId: four 35 | 36 | --- 37 | id: four 38 | title: "Four" 39 | branches: 40 | - message: "Five" 41 | targetId: five 42 | 43 | --- 44 | id: five 45 | title: "Five" 46 | branches: 47 | - message: "Six" 48 | targetId: six 49 | 50 | --- 51 | id: six 52 | title: "Six" 53 | branches: 54 | - message: "Seven" 55 | targetId: seven 56 | 57 | --- 58 | id: seven 59 | title: "Seven" 60 | branches: 61 | - message: "Eight" 62 | targetId: eight 63 | 64 | --- 65 | id: eight 66 | title: "Eight" 67 | branches: 68 | - message: "Nine" 69 | targetId: nine 70 | 71 | --- 72 | id: nine 73 | title: "Nine" 74 | branches: 75 | - message: "Zero" 76 | targetId: zero 77 | -------------------------------------------------------------------------------- /src/test/resources/models/missing_node.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "How may I help you?" 4 | branches: 5 | - message: "I need help with the Aki Bot" 6 | targetId: bot-root 7 | - message: "I need help with the Aki Server" 8 | targetId: server-root 9 | -------------------------------------------------------------------------------- /src/test/resources/models/no_root_node.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: bot-root 3 | title: "You need help with the Aki Bot. What's wrong?" 4 | branches: 5 | - message: "Something is broken" 6 | targetId: bot-broken 7 | - message: "I want to change a setting." 8 | targetId: bot-setting 9 | - message: "I have a suggestion." 10 | targetId: bot-suggestion 11 | 12 | --- 13 | id: server-root 14 | title: "Denied." 15 | 16 | --- 17 | id: bot-broken 18 | title: "Aki never breaks." 19 | 20 | --- 21 | id: bot-setting 22 | title: "Aki has no settings to be changed." 23 | 24 | --- 25 | id: bot-suggestion 26 | title: "We don't take any suggestions currently." 27 | -------------------------------------------------------------------------------- /src/test/resources/models/root_loop.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "Foo" 4 | branches: 5 | - message: "Bar" 6 | targetId: foo-bar 7 | 8 | --- 9 | id: foo-bar 10 | title: "Whatever" 11 | branches: 12 | - message: "Loop" 13 | targetId: root 14 | -------------------------------------------------------------------------------- /src/test/resources/models/sane.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "How may I help you?" 4 | branches: 5 | - message: "I need help with the Aki Bot" 6 | targetId: bot-root 7 | - message: "I need help with the Aki Server" 8 | targetId: server-root 9 | 10 | --- 11 | id: bot-root 12 | title: "You need help with the Aki Bot. What's wrong?" 13 | branches: 14 | - message: "Something is broken" 15 | targetId: bot-broken 16 | - message: "I want to change a setting." 17 | targetId: bot-setting 18 | - message: "I have a suggestion." 19 | targetId: bot-suggestion 20 | - message: "None of the above." 21 | targetId: support-role 22 | 23 | --- 24 | id: server-root 25 | title: "Denied." 26 | 27 | --- 28 | id: bot-broken 29 | title: "Aki never breaks." 30 | 31 | --- 32 | id: bot-setting 33 | title: "Aki has no settings to be changed." 34 | 35 | --- 36 | id: bot-suggestion 37 | title: "We don't take any suggestions currently." 38 | 39 | --- 40 | id: support-role 41 | title: "Explain your problem to one of our helpers in <#487925562300694531>." 42 | roleId: 487925645989380108 43 | -------------------------------------------------------------------------------- /src/test/resources/models/unreferenced_node.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: root 3 | title: "How may I help you?" 4 | branches: 5 | - message: "I need help with the Aki Bot" 6 | targetId: bot-root 7 | - message: "I need help with the Aki Server" 8 | targetId: server-root 9 | 10 | --- 11 | id: bot-root 12 | title: "You need help with the Aki Bot. What's wrong?" 13 | branches: 14 | - message: "Something is broken" 15 | targetId: bot-broken 16 | - message: "I want to change a setting." 17 | targetId: bot-setting 18 | 19 | --- 20 | id: server-root 21 | title: "Denied." 22 | 23 | --- 24 | id: bot-broken 25 | title: "Aki never breaks." 26 | 27 | --- 28 | id: bot-setting 29 | title: "Aki has no settings to be changed." 30 | 31 | --- 32 | id: bot-suggestion 33 | title: "We don't take any suggestions currently." 34 | --------------------------------------------------------------------------------