Skip to content

Commit

Permalink
Adding a discord adapter (#223)
Browse files Browse the repository at this point in the history
Implemented a Discord Adapter for yetibot

Co-authored-by: Trevor Hartman <trevorhartman@gmail.com>

---------

Co-authored-by: Trevor Hartman <trevorhartman@gmail.com>
  • Loading branch information
bensontrinh and devth committed Jan 11, 2024
1 parent 1f4ccf7 commit 9c099fd
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 2 deletions.
2 changes: 2 additions & 0 deletions config/config.sample.edn
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
:token "xoxb-111111111111111111111111111111111111"}
:k8s {:type "slack",
:token "xoxb-9999999999999999"}
:mydiscord {:type "discord"
:token "mt111111111111111111111"}
:freenode {:type "irc",
:username "yetibot",
:host "chat.freenode.net",
Expand Down
3 changes: 3 additions & 0 deletions config/profiles.sample.clj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
:yetibot-adapters-freenode-ssl "true"
:yetibot-adapters-freenode-username "yetibot"

:yetibot-adapters-mydiscord-type "discord"
:yetibot-adapters-mydiscord-token "mt111111111111111111111"

:yetibot-adapters-mymattermost-type "mattermost"
:yetibot-adapters-mymattermost-host "yetibot-mattermost.herokuapp.com"
:yetibot-adapters-mymattermost-token "h1111111111111111111111111"
Expand Down
3 changes: 3 additions & 0 deletions config/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ YETIBOT_ADAPTERS_MYTEAM_TOKEN="xoxb-111111111111111111111111111111111111"
YETIBOT_ADAPTERS_K8S_TYPE="slack"
YETIBOT_ADAPTERS_K8S_TOKEN="xoxb-k8s-slack-9999999999999999"

YETIBOT_ADAPTERS_MYDISCORD_TYPE="discord"
YETIBOT_ADAPTERS_MYDISCORD_TOKEN="mt111111111111111111111"

YETIBOT_ADAPTERS_FREENODE_TYPE="irc"
YETIBOT_ADAPTERS_FREENODE_HOST="chat.freenode.net"
YETIBOT_ADAPTERS_FREENODE_PORT="7070"
Expand Down
4 changes: 3 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,12 @@
; chat protocols
[irclj "0.5.0-alpha4"]
;; use this fork which uses javax.websockets for compatability with Yetibot
[stylefruits/gniazdo-jsr356 "1.0.0"]
;; gniazdo 1.2.2 needed for discljord
[stylefruits/gniazdo "1.2.2"]
[slack-rtm "0.1.7" :exclusions [[stylefruits/gniazdo]]]
[org.julienxx/clj-slack "0.6.3"]
[mattermost-clj "4.0.3"]
[com.github.discljord/discljord "1.3.1"]

; javascript evaluation
[evaljs "0.1.2"]
Expand Down
5 changes: 4 additions & 1 deletion src/yetibot/core/adapters.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[yetibot.core.adapters.slack :as slack]
[yetibot.core.adapters.mattermost :as mattermost]
[yetibot.core.adapters.web :as web]
[yetibot.core.adapters.discord :as discord]
[taoensso.timbre :as log :refer [info debug warn]]
[clojure.stacktrace :refer [print-stack-trace]]
[yetibot.core.adapters.adapter :as a]
Expand All @@ -14,7 +15,8 @@
(s/def ::adapter (s/or :web ::web/config
:slack ::slack/config
:irc ::irc/config
:mattermost ::mattermost/config))
:mattermost ::mattermost/config
:discord ::discord/config))

(s/def ::config (s/map-of keyword? ::adapter))

Expand Down Expand Up @@ -45,6 +47,7 @@
:slack (slack/make-slack config)
:irc (irc/make-irc config)
:mattermost (mattermost/make-mattermost config)
:discord (discord/make-discord config)
(throw (ex-info (str "Unknown adapter type " (:type config)) config))))

(defn ->registerable-adapter
Expand Down
173 changes: 173 additions & 0 deletions src/yetibot/core/adapters/discord.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
(ns yetibot.core.adapters.discord
(:require [clojure.spec.alpha :as spec]
[clojure.core.async :as async]
[discljord.messaging :as messaging]
[discljord.connections :as discord-ws]
[yetibot.core.models.users :as users]
[discljord.events :as events]
[taoensso.timbre :as timbre]
[yetibot.core.adapters.adapter :as adapter]
[yetibot.core.handler :as handler]
[yetibot.core.chat :as chat]))

(spec/def ::type #{"discord"})
(spec/def ::token string?)
(spec/def ::config (spec/keys :req-un [::type ::token]))

(defmulti handle-event
(fn [event-type event-data _conn yetibot-user]
event-type))

;; also has :message-reaction-remove
(defmethod handle-event :message-reaction-add
[event-type event-data _conn yetibot-user]
(let [message-id (:message-id event-data)
channel-id (:channel-id event-data)
message-author-id (:message-author-id event-data)
emoji-name (-> event-data
:emoji
:name)
rest-conn (:rest @_conn)
yetibot? (= message-author-id (:id @yetibot-user))]
(if (and
(= emoji-name "")
(= yetibot? true))
(messaging/delete-message! rest-conn channel-id message-id)
(if (= yetibot? true)
(timbre/debug "We don't handle" emoji-name "from yetibot")
(let [cs (assoc (chat/chat-source channel-id)
:raw-event event-data)
user-model (assoc (users/get-user cs message-author-id)
:yetibot? yetibot?)
message-content (:content @(messaging/get-channel-message! rest-conn channel-id message-id))]
(handler/handle-raw
cs
user-model
:react
@yetibot-user
{:reaction emoji-name
:body message-content
:message-user message-author-id}))))))


(defmethod handle-event :message-create
[event-type event-data _conn yetibot-user]
(if (not= (:id @yetibot-user) (-> event-data
:author
:id))
(let [user-model (users/create-user
(-> event-data
:author
:username)
(event-data :author :id))
message (:content event-data)
cs (assoc (chat/chat-source (:channel-id event-data))
:raw-event event-data)]
(binding [chat/*target* (:channel-id event-data)]
(handler/handle-raw
cs
user-model
:message
@yetibot-user
{:body message})))
(timbre/debug "Message from Yetibot => ignoring")))

(defn start
"start the discord connection"
[{conn :conn
config :config
connected? :connected?
bot-id :bot-id
yetibot-user :yetibot-user :as adapter}]
(timbre/debug "starting discord connection")

(binding [chat/*adapter* adapter]
(let [event-channel (async/chan 100)
message-channel (discord-ws/connect-bot! (:token config) event-channel :intents #{:guilds :guild-messages :guild-message-reactions :direct-messages :direct-message-reactions})
rest-connection (messaging/start-connection! (:token config))
retcon {:event event-channel
:message message-channel
:rest rest-connection}]
(reset! conn retcon)

(timbre/debug (pr-str conn))

(reset! connected? true)
(reset! bot-id {:id @(messaging/get-current-user! rest-connection)})
(reset! yetibot-user @(messaging/get-current-user! rest-connection))
(events/message-pump! event-channel (fn [event-type event-data] (handle-event event-type event-data conn yetibot-user))))))

(defn- channels [a]
(let [guild-channels (messaging/get-guild-channels!)]
(timbre/debug "Guild Channels: " (pr-str guild-channels))
(guild-channels)))

(defn- send-msg [{:keys [conn]} msg]
(messaging/create-message! (:rest @conn) chat/*target* :content msg))

(defn stop
"stop the discord connection"
[{:keys [conn] :as adapter}]
(timbre/debug "Closing Discord" (adapter/uuid adapter))
(messaging/stop-connection! (:message @conn))
(async/close! (:event @conn)))

(defrecord Discord
[config
bot-id
conn
connected?
connection-last-active-timestamp
connection-latency
should-ping?
yetibot-user]

adapter/Adapter

(a/uuid [_] (:name config))

(a/platform-name [_] "Discord")

(a/channels [a] (channels a))

(a/send-paste [a msg] (send-msg a msg))

(a/send-msg [a msg] (send-msg a msg))

(a/join [_ channel]
(str
"Discord bots such as myself can't join channels on their own. Use "
"/invite from the channel you'd like me to join instead.✌️"))

(a/leave [_ channel]
(str
"Discord bots such as myself can't leave channels on their own. Use "
"/kick from the channel you'd like me to leave instead. 👊"))

(a/chat-source [_ channel] (chat/chat-source channel))

(a/stop [adapter] (stop adapter))

(a/connected? [{:keys [connected?]}]
@connected?)

(a/connection-last-active-timestamp [_]
@connection-last-active-timestamp)

(a/connection-latency [_]
@connection-latency)

(a/start [adapter]
(start adapter)))

(defn make-discord
[config]
(map->Discord
{:config config
:bot-id (atom nil)
:conn (atom nil)
:connected? (atom false)
:connection-latency (atom nil)
:connection-last-active-timestamp (atom nil)
:yetibot-user (atom nil)
:should-ping? (atom false)}))
6 changes: 6 additions & 0 deletions test/yetibot/core/test/adapters.clj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
"makes mattermost adapter"
(instance? yetibot.core.adapters.mattermost.Mattermost
(a/make-adapter {:type "mattermost"})) => true)

(fact
"makes discord adapter"
(instance? yetibot.core.adapters.discord.Discord
(a/make-adapter {:type "discord"})) => true)

(fact
"throws exception for unknown adapter"
(a/make-adapter {:type "throwme"}) => (throws Exception)))
Expand Down
65 changes: 65 additions & 0 deletions test/yetibot/core/test/adapters/discord.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
(ns yetibot.core.test.adapters.discord
(:require [yetibot.core.adapters.discord :as discord]
[discljord.messaging :as messaging]
[yetibot.core.handler :as handler]
[yetibot.core.models.users :as users]
[yetibot.core.chat :as chat]
[midje.sweet :refer [fact facts anything => provided]]))

(facts
"about handle-event message-reaction-add"
(fact
"deletes yetibot message when reacted with x"
(discord/handle-event :message-reaction-add
{:message-id 123
:channel-id 456
:message-author-id 111
:emoji {:name ""}}
(atom nil)
(atom {:id 111})) => "I did it"
(provided (messaging/delete-message! anything anything anything) => "I did it"))
(fact
"when reacting to a non delete yetibot message do nothing"
(discord/handle-event :message-reaction-add
{:message-id 123
:channel-id 456
:message-author-id 111
:emoji {:name "🍿"}}
(atom nil)
(atom {:id 111})) => nil)
(fact
"when reacting to a non delete user message handle-raw is called"
(let [mock-promise (def x (promise))]
(discord/handle-event :message-reaction-add
{:message-id 123
:channel-id 456
:message-author-id 999
:emoji {:name "🍿"}}
(atom nil)
(atom {:id 111})) => "called handle-raw"
(provided (handler/handle-raw anything anything anything anything anything) => "called handle-raw"
(chat/chat-source 456) => {:channel-id 456 :room "fake"}
(users/get-user anything anything) => {:id 888}
(messaging/get-channel-message! anything 456 123) => (deliver mock-promise "fake content")))))


(facts
"about message creation"
(fact
"ignore yetibot messages"
(discord/handle-event :message-create
{:author {:id 123}}
(atom nil)
(atom {:id 123})) => nil)
(fact
"handles user messages"
(discord/handle-event :message-create
{:author {:id 999 :username "fake"}
:channel-id 456
:content "fake content eh"}
(atom nil)
(atom {:id 123})) => "called handle-raw"
(provided (users/create-user "fake" {:id 999 :username "fake"}) => {:username "fake"}
(chat/chat-source 456) => {:channel-id 456 :room "fake"}
(handler/handle-raw anything anything anything anything anything) => "called handle-raw")))

0 comments on commit 9c099fd

Please sign in to comment.