Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a discord adapter #223

Merged
merged 23 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config.sample.edn
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to update this for discord to work properly. I'm thinking this will end up breaking other things since this uses jetty's Websocket

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like your local yb is running just fine? I'll do some more testing but this upgrade might be good to go.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah locally it is fine. I didn't test any other connectors though.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to include this dep in the the yetibot/yetibotproject.clj as well to get it to run properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed slack still works with this upgrade.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sick. Thanks for checking that out.

[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
devth marked this conversation as resolved.
Show resolved Hide resolved
(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
(= (-> event-data :emoji :name) "❌")
bensontrinh marked this conversation as resolved.
Show resolved Hide resolved
(= 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")))

Loading