├── .gitignore ├── LICENSE ├── README.md ├── check.sh ├── project.clj └── src └── clj_discord ├── core.clj └── example.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /.classpath 2 | /.project 3 | /target/ 4 | /bin/ 5 | /.lein-repl-history 6 | /.nrepl-port 7 | /pom.xml 8 | /token.txt 9 | /.eastwood 10 | /.idea/ 11 | /clj-discord.iml 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-discord 2 | 3 | ## Clojure library for using the Discord API 4 | 5 | This is a very minimalist library, but if you just want to build a simple bot 6 | that reacts to certain commands and events by sending chat messages, it should be sufficient. 7 | 8 | ## To use this library: 9 | 10 | 1. clone this repository 11 | 2. do `lein install` 12 | 3. add to your project dependencies `[clj-discord "0.1.0-SNAPSHOT"]` 13 | 4. add to your namespace declaration `(:require [clj-discord.core :as discord])` 14 | 5. have a look at the code in the namespace clj-discord.example 15 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | # format the code 2 | lein cljfmt fix 3 | lein cljfmt fix project.clj 4 | 5 | # check for updatable dependencies and for dependency conflicts 6 | lein ancient 7 | lein ancient :plugins 8 | lein deps :tree > /dev/null # we are only interested in stderr 9 | lein deps :plugin-tree > /dev/null # we are only interested in stderr 10 | 11 | # run the linters 12 | touch token.txt 13 | lein eastwood 14 | rm token.txt 15 | lein kibit 16 | lein clj-kondo --lint src 17 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-discord "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.10.3"] 3 | [clj-http "3.12.3"] 4 | [org.clojure/data.json "2.4.0"] 5 | [stylefruits/gniazdo "1.0.0"]] 6 | 7 | :plugins [[lein-ancient "1.0.0-RC3"] ;; finds updatable dependencies 8 | [lein-cljfmt "0.8.0"] ;; for formatting Clojure code 9 | [jonase/eastwood "1.0.0"] ;; a Clojure linter 10 | [lein-kibit "0.1.8"] ;; another linter, for both Clojure and ClojureScript 11 | [com.github.clj-kondo/lein-clj-kondo "0.1.3"] ;; and one more linter, for both Clojure and ClojureScript 12 | [org.clojure/clojure "1.10.3"]] ;; making sure the plugins use the latest Clojure 13 | 14 | :main clj-discord.example 15 | :aot :all) 16 | -------------------------------------------------------------------------------- /src/clj_discord/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-discord.core 2 | (:gen-class) 3 | (:require [clojure.java.io :as io] 4 | [clj-http.client :as http] 5 | [clojure.data.json :as json] 6 | [gniazdo.core :as ws]) 7 | (:import (org.eclipse.jetty.util.ssl SslContextFactory) 8 | (org.eclipse.jetty.websocket.client WebSocketClient))) 9 | 10 | (defonce previous-bot (atom -1)) 11 | (defonce bots (atom {})) 12 | 13 | (defn disconnect 14 | ([] 15 | (disconnect [0])) 16 | ([bot] 17 | (when (contains? @bots bot) 18 | (let [socket (:socket (get @bots bot)) 19 | websocket-client (:websocket-client (get @bots bot))] 20 | (when-not (nil? socket) (ws/close socket)) 21 | (when-not (nil? websocket-client) (.stop ^WebSocketClient websocket-client)) 22 | (swap! bots dissoc bot))))) 23 | 24 | (defn connect [params] 25 | (let [{:keys [token functions log-events? log-function max-text-message-size bot rate-limit] 26 | :or {functions {} 27 | log-events? true 28 | log-function (fn [& args] (println "\n" args)) 29 | max-text-message-size (* 64 1024) 30 | rate-limit 5000 31 | bot (swap! previous-bot inc)}} params 32 | token (if (.startsWith ^String token "Bot ") (.trim ^String token) (str "Bot " (.trim ^String token))) 33 | params {:token token 34 | :functions functions 35 | :log-events? log-events? 36 | :log-function log-function 37 | :max-text-message-size max-text-message-size 38 | :bot bot 39 | :rate-limit rate-limit} 40 | 41 | websocket-client (new WebSocketClient (new SslContextFactory)) 42 | heartbeat-thread (Thread. ^Runnable (fn [] 43 | (while (:keep-alive (get @bots bot)) 44 | (try 45 | (if (nil? (:heartbeat-interval (get @bots bot))) 46 | (Thread/sleep 100) 47 | (do 48 | (when log-events? (log-function "Sending heartbeat " (:seq (get @bots bot)))) 49 | (ws/send-msg (:socket (get @bots bot)) (json/write-str {:op 1, :d (:seq (get @bots bot))})) 50 | (Thread/sleep (:heartbeat-interval (get @bots bot))))) 51 | (catch Exception e (do 52 | (log-function "Caught exception: " (.getMessage e)) 53 | (swap! bots update-in [bot] assoc :keep-alive false)))))))] 54 | 55 | (when (contains? @bots bot) 56 | (disconnect bot) 57 | (Thread/sleep 5000)) 58 | 59 | (swap! bots assoc bot {:params params 60 | :keep-alive true 61 | :gateway (str 62 | (get 63 | (json/read-str 64 | (:body (http/get "https://discordapp.com/api/gateway" 65 | {:headers {:authorization token}}))) 66 | "url") 67 | "?v=6&encoding=json") 68 | :websocket-client websocket-client 69 | :heartbeat-thread heartbeat-thread 70 | :control-thread (Thread/currentThread)}) 71 | 72 | (.setMaxTextMessageSize (.getPolicy websocket-client) max-text-message-size) 73 | (.start websocket-client) 74 | 75 | (swap! bots update-in [bot] assoc :socket 76 | (ws/connect 77 | (:gateway (get @bots bot)) 78 | :client 79 | websocket-client 80 | :on-receive 81 | #(let [received (json/read-str %) 82 | _ (when log-events? (log-function %)) 83 | op (get received "op") 84 | type (get received "t") 85 | data (get received "d") 86 | seq (get received "s")] 87 | (when (= 10 op) (swap! bots update-in [bot] assoc :heartbeat-interval (get data "heartbeat_interval"))) 88 | (when-not (nil? seq) (swap! bots update-in [bot] assoc :seq seq)) 89 | (when-not (nil? type) (doseq [afunction (get functions type (get functions "ALL_OTHER" []))] 90 | (afunction type data)))))) 91 | 92 | (.start heartbeat-thread) 93 | (Thread/sleep 1000) 94 | 95 | (ws/send-msg (:socket (get @bots bot)) (json/write-str {:op 2, :d {"token" token 96 | "properties" {"$os" "linux" 97 | "$browser" "clj-discord" 98 | "$device" "clj-discord" 99 | "$referrer" "" 100 | "$referring_domain" ""} 101 | "compress" false}})) 102 | 103 | (while (:keep-alive (get @bots bot)) (Thread/sleep 1000)) 104 | (try 105 | (connect params) 106 | (catch Exception e 107 | (do 108 | (log-function "Caught exception: " (.getMessage e)) 109 | (Thread/sleep 60000) 110 | (try 111 | (connect params) 112 | (catch Exception e 113 | (do 114 | (log-function "Caught exception: " (.getMessage e)) 115 | (log-function "Abandoning the attempts to reconnect bot " bot) 116 | (disconnect bot))))))))) 117 | 118 | (defn connect-without-blocking [params] 119 | (let [params-contain-bot (contains? params :bot) 120 | bot (if params-contain-bot (:bot params) (swap! previous-bot inc)) 121 | _ (when-not (integer? bot) (throw (Exception. "Malformed bot identifier!"))) 122 | params (assoc params :bot bot)] 123 | (.start (Thread. ^Runnable (fn [] (connect params)))) 124 | bot)) 125 | 126 | (defn check-rate-limit [bot] 127 | (let [now (System/currentTimeMillis) 128 | previous-activity (:activity (get @bots bot)) 129 | previous-activity (if (nil? previous-activity) 0 previous-activity) 130 | millis-since-previous-activity (- now previous-activity) 131 | rate-limit (-> (get @bots bot) :params :rate-limit)] 132 | (if 133 | (> rate-limit millis-since-previous-activity) 134 | false 135 | (do 136 | (swap! bots update-in [bot] assoc :activity now) 137 | true)))) 138 | 139 | (defn post-message 140 | ([channel-id message] 141 | (post-message 0 channel-id message)) 142 | ([bot channel-id message] 143 | (when (check-rate-limit bot) 144 | (http/post (str "https://discordapp.com/api/channels/" channel-id "/messages") 145 | {:body (json/write-str {:content message 146 | :nonce (str (System/currentTimeMillis)) 147 | :tts false}) 148 | :headers {:authorization (:token (:params (get @bots bot)))} 149 | :content-type :json 150 | :accept :json})))) 151 | 152 | (defn post-message-with-file 153 | ([channel-id message filename] 154 | (post-message-with-file 0 channel-id message filename)) 155 | ([bot channel-id message filename] 156 | (when (check-rate-limit bot) 157 | (http/post (str "https://discordapp.com/api/channels/" channel-id "/messages") 158 | {:multipart [{:name "content" :content message} 159 | {:name "nonce" :content (str (System/currentTimeMillis))} 160 | {:name "tts" :content "false"} 161 | {:name filename :part_name "file" :content (io/file filename)}] 162 | :headers {:authorization (:token (:params (get @bots bot)))}})))) 163 | 164 | (defn post-message-with-mention 165 | ([channel-id message user-id] 166 | (post-message-with-mention 0 channel-id message user-id)) 167 | ([bot channel-id message user-id] 168 | (post-message bot channel-id (str "<@" user-id ">" message)))) 169 | 170 | (defn answer-command 171 | ([data command answer] 172 | (answer-command 0 data command answer)) 173 | ([bot data command answer] 174 | (when (= command (get data "content")) 175 | (post-message-with-mention 176 | bot 177 | (get data "channel_id") 178 | (str " " answer) 179 | (get (get data "author") "id"))))) 180 | 181 | (defn delete-message 182 | ([data command] 183 | (delete-message 0 data command)) 184 | ([bot data command] 185 | (let [channel_id (get data "channel_id") message (get data "id")] 186 | (when (and (check-rate-limit bot) (= command (get data "content"))) 187 | (http/delete (str "https://discordapp.com/api/channels/" channel_id "/messages/" message "?token=" (:token (:params (get @bots bot)))) {:throw-exceptions false}))))) 188 | -------------------------------------------------------------------------------- /src/clj_discord/example.clj: -------------------------------------------------------------------------------- 1 | (ns clj-discord.example 2 | (:gen-class) 3 | (:require [clj-discord.core :as discord])) 4 | 5 | (defonce token (.trim (slurp "token.txt"))) 6 | 7 | (defn d100 [_ data] 8 | (discord/answer-command data "!d100" (str "Here you are a random number between 1 and 100: " (inc (rand-int 100))))) 9 | 10 | (defn log-event [type data] 11 | (println "\nReceived: " type " -> " data)) 12 | 13 | (defn -main [] 14 | (discord/connect {:token token 15 | :functions {"MESSAGE_CREATE" [d100] 16 | "MESSAGE_UPDATE" [d100] 17 | "ALL_OTHER" [log-event]}})) 18 | 19 | ;(discord/disconnect) 20 | --------------------------------------------------------------------------------