Prevent send button spamming in public chats
This commit is contained in:
parent
71ff9d9035
commit
00c5c630f4
1
.env
1
.env
|
@ -19,3 +19,4 @@ INSTABUG_SURVEYS=1
|
|||
GROUP_CHATS_ENABLED=0
|
||||
FORCE_SENT_RECEIVED_TRACKING=0
|
||||
USE_SYM_KEY=0
|
||||
SPAM_BUTTON_DETECTION_ENABLED=1
|
|
@ -19,3 +19,4 @@ INSTABUG_SURVEYS=1
|
|||
GROUP_CHATS_ENABLED=0
|
||||
FORCE_SENT_RECEIVED_TRACKING=1
|
||||
USE_SYM_KEY=0
|
||||
SPAM_BUTTON_DETECTION_ENABLED=1
|
||||
|
|
|
@ -18,3 +18,4 @@ DEBUG_WEBVIEW=1
|
|||
INSTABUG_SURVEYS=1
|
||||
GROUP_CHATS_ENABLED=0
|
||||
FORCE_SENT_RECEIVED_TRACKING=1
|
||||
SPAM_BUTTON_DETECTION_ENABLED=1
|
||||
|
|
|
@ -12,3 +12,12 @@
|
|||
;; TODO(janherich): figure out something better then this
|
||||
(def send-command-ref ["transactor" :command 83 "send"])
|
||||
(def request-command-ref ["transactor" :command 83 "request"])
|
||||
|
||||
(def spam-message-frequency-threshold 4)
|
||||
(def spam-interval-ms 1000)
|
||||
(def default-cooldown-period-ms 10000)
|
||||
(def cooldown-reset-threshold 3)
|
||||
(def cooldown-periods-ms
|
||||
{1 2000
|
||||
2 5000
|
||||
3 10000})
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[status-im.utils.handlers :as handlers]
|
||||
[status-im.utils.handlers-macro :as handlers-macro]
|
||||
[status-im.utils.contacts :as utils.contacts]
|
||||
[status-im.utils.utils :as utils]
|
||||
[status-im.transport.message.core :as transport.message]
|
||||
[status-im.transport.message.v1.protocol :as protocol]
|
||||
[status-im.transport.message.v1.public-chat :as public-chat]
|
||||
|
@ -34,6 +35,13 @@
|
|||
(fn [link]
|
||||
(list-selection/browse link)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:show-cooldown-warning
|
||||
(fn [_]
|
||||
(utils/show-popup nil
|
||||
(i18n/label :cooldown/warning-message)
|
||||
#())))
|
||||
|
||||
;;;; Handlers
|
||||
|
||||
(handlers/register-handler-db
|
||||
|
@ -411,3 +419,9 @@
|
|||
[re-frame/trim-v]
|
||||
(fn [cofx [chat-id message-id]]
|
||||
(models.message/delete-message chat-id message-id cofx)))
|
||||
|
||||
(handlers/register-handler-db
|
||||
:disable-cooldown
|
||||
[re-frame/trim-v]
|
||||
(fn [db]
|
||||
(assoc db :chat/cooldown-enabled? false)))
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
(and (multi-user-chat? chat-id cofx)
|
||||
(not (get-in cofx [:db :chats chat-id :public?]))))
|
||||
|
||||
(defn public-chat? [chat-id cofx]
|
||||
(get-in cofx [:db :chats chat-id :public?]))
|
||||
|
||||
(defn set-chat-ui-props
|
||||
"Updates ui-props in active chat by merging provided kvs into them"
|
||||
[{:keys [current-chat-id] :as db} kvs]
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
(:require [clojure.string :as str]
|
||||
[goog.object :as object]
|
||||
[status-im.chat.constants :as const]
|
||||
[status-im.chat.models :as chat-model]
|
||||
[status-im.chat.models.commands :as commands-model]
|
||||
[status-im.js-dependencies :as dependencies]
|
||||
[taoensso.timbre :as log]))
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.utils.datetime :as datetime]
|
||||
[status-im.js-dependencies :as dependencies]))
|
||||
|
||||
(defn text->emoji
|
||||
"Replaces emojis in a specified `text`"
|
||||
|
@ -198,3 +200,34 @@
|
|||
[(keyword (get-in params [i :name])) value]))
|
||||
(remove #(nil? (first %)))
|
||||
(into {}))))
|
||||
|
||||
(defn- start-cooldown [{:keys [db]} cooldowns]
|
||||
{:dispatch-later [{:dispatch [:disable-cooldown]
|
||||
:ms (const/cooldown-periods-ms cooldowns
|
||||
const/default-cooldown-period-ms)}]
|
||||
:show-cooldown-warning nil
|
||||
:db (assoc db
|
||||
:chat/cooldowns (if (= const/cooldown-reset-threshold cooldowns)
|
||||
0
|
||||
cooldowns)
|
||||
:chat/spam-messages-frequency 0
|
||||
:chat/cooldown-enabled? true)})
|
||||
|
||||
(defn process-cooldown [{{:keys [chat/last-outgoing-message-sent-at
|
||||
chat/cooldowns
|
||||
chat/spam-messages-frequency
|
||||
current-chat-id] :as db} :db :as cofx}]
|
||||
(when (and
|
||||
config/spam-button-detection-enabled?
|
||||
(chat-model/public-chat? current-chat-id cofx))
|
||||
(let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at)
|
||||
(+ const/spam-interval-ms (* 1000 cooldowns)))
|
||||
spamming-frequently? (= const/spam-message-frequency-threshold spam-messages-frequency)]
|
||||
(cond-> {:db (assoc db
|
||||
:chat/last-outgoing-message-sent-at (datetime/timestamp)
|
||||
:chat/spam-messages-frequency (if spamming-fast?
|
||||
(inc spam-messages-frequency)
|
||||
0))}
|
||||
|
||||
(and spamming-fast? spamming-frequently?)
|
||||
(start-cooldown (inc cooldowns))))))
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
[status-im.chat.events.requests :as requests-events]
|
||||
[status-im.chat.models :as chat-model]
|
||||
[status-im.chat.models.commands :as commands-model]
|
||||
[status-im.chat.models.input :as input]
|
||||
[status-im.utils.clocks :as utils.clocks]
|
||||
[status-im.utils.handlers-macro :as handlers-macro]
|
||||
[status-im.utils.money :as money]
|
||||
|
@ -298,6 +299,7 @@
|
|||
message-id (transport.utils/message-id send-record)
|
||||
message-with-id (assoc message :message-id message-id)]
|
||||
(handlers-macro/merge-fx cofx
|
||||
(input/process-cooldown)
|
||||
(chat-model/upsert-chat {:chat-id chat-id
|
||||
:timestamp now})
|
||||
(add-single-message message-with-id true)
|
||||
|
|
|
@ -21,3 +21,7 @@
|
|||
(s/def :chat/last-clock-value (s/nilable number?)) ; last logical clock value of messages in chat
|
||||
(s/def :chat/loaded-chats (s/nilable seq?))
|
||||
(s/def :chat/bot-db (s/nilable map?))
|
||||
(s/def :chat/cooldowns (s/nilable number?)) ; number of cooldowns given for spamming send button
|
||||
(s/def :chat/cooldown-enabled? (s/nilable boolean?))
|
||||
(s/def :chat/last-outgoing-message-sent-at (s/nilable number?))
|
||||
(s/def :chat/spam-messages-frequency (s/nilable number?)) ; number of consecutive spam messages sent
|
||||
|
|
|
@ -413,3 +413,16 @@
|
|||
:wallet-transaction-exists?
|
||||
(fn [db [_ tx-hash]]
|
||||
(not (nil? (get-in db [:wallet :transactions tx-hash])))))
|
||||
|
||||
(reg-sub
|
||||
:chat/cooldown-enabled?
|
||||
(fn [db]
|
||||
(:chat/cooldown-enabled? db)))
|
||||
|
||||
(reg-sub
|
||||
:chat-cooldown-enabled?
|
||||
:<- [:get-current-chat]
|
||||
:<- [:chat/cooldown-enabled?]
|
||||
(fn [[{:keys [public?]} cooldown-enabled?]]
|
||||
(and public?
|
||||
cooldown-enabled?)))
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[status-im.chat.views.input.send-button :as send-button]
|
||||
[status-im.chat.views.input.suggestions :as suggestions]
|
||||
[status-im.chat.views.input.validation-messages :as validation-messages]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.ui.components.animation :as animation]
|
||||
[status-im.ui.components.colors :as colors]
|
||||
[status-im.ui.components.react :as react]
|
||||
|
@ -27,49 +28,53 @@
|
|||
(defview basic-text-input [{:keys [set-layout-height-fn set-container-width-fn height single-line-input?]}]
|
||||
(letsubs [{:keys [input-text]} [:get-current-chat]
|
||||
input-focused? [:get-current-chat-ui-prop :input-focused?]
|
||||
input-ref (atom nil)]
|
||||
input-ref (atom nil)
|
||||
cooldown-enabled? [:chat-cooldown-enabled?]]
|
||||
[react/text-input
|
||||
{:ref #(when %
|
||||
(re-frame/dispatch [:set-chat-ui-props {:input-ref %}])
|
||||
(reset! input-ref %))
|
||||
:accessibility-label :chat-message-input
|
||||
:multiline (not single-line-input?)
|
||||
:default-value (or input-text "")
|
||||
:editable true
|
||||
:blur-on-submit false
|
||||
:on-focus #(re-frame/dispatch [:set-chat-ui-props {:input-focused? true
|
||||
:messages-focused? false}])
|
||||
:on-blur #(re-frame/dispatch [:set-chat-ui-props {:input-focused? false}])
|
||||
:on-submit-editing (fn [_]
|
||||
(if single-line-input?
|
||||
(re-frame/dispatch [:send-current-message])
|
||||
(when @input-ref
|
||||
(.setNativeProps @input-ref (clj->js {:text input-text})))))
|
||||
:on-layout (fn [e]
|
||||
(set-container-width-fn (.-width (.-layout (.-nativeEvent e)))))
|
||||
:on-change (fn [e]
|
||||
(let [native-event (.-nativeEvent e)
|
||||
text (.-text native-event)
|
||||
content-size (.. native-event -contentSize)]
|
||||
(when (and (not single-line-input?)
|
||||
content-size)
|
||||
(set-layout-height-fn (.-height content-size)))
|
||||
(when (not= text input-text)
|
||||
(re-frame/dispatch [:set-chat-input-text text]))))
|
||||
:on-content-size-change (when (and (not input-focused?)
|
||||
(not single-line-input?))
|
||||
#(let [s (.-contentSize (.-nativeEvent %))
|
||||
w (.-width s)
|
||||
h (.-height s)]
|
||||
(set-container-width-fn w)
|
||||
(set-layout-height-fn h)))
|
||||
:on-selection-change #(let [s (-> (.-nativeEvent %)
|
||||
(.-selection))
|
||||
end (.-end s)]
|
||||
(re-frame/dispatch [:update-text-selection end]))
|
||||
:style (style/input-view single-line-input?)
|
||||
:placeholder-text-color colors/gray
|
||||
:auto-capitalize :sentences}]))
|
||||
(merge
|
||||
{:ref #(when %
|
||||
(re-frame/dispatch [:set-chat-ui-props {:input-ref %}])
|
||||
(reset! input-ref %))
|
||||
:accessibility-label :chat-message-input
|
||||
:multiline (not single-line-input?)
|
||||
:default-value (or input-text "")
|
||||
:editable (not cooldown-enabled?)
|
||||
:blur-on-submit false
|
||||
:on-focus #(re-frame/dispatch [:set-chat-ui-props {:input-focused? true
|
||||
:messages-focused? false}])
|
||||
:on-blur #(re-frame/dispatch [:set-chat-ui-props {:input-focused? false}])
|
||||
:on-submit-editing (fn [_]
|
||||
(if single-line-input?
|
||||
(re-frame/dispatch [:send-current-message])
|
||||
(when @input-ref
|
||||
(.setNativeProps @input-ref (clj->js {:text input-text})))))
|
||||
:on-layout (fn [e]
|
||||
(set-container-width-fn (.-width (.-layout (.-nativeEvent e)))))
|
||||
:on-change (fn [e]
|
||||
(let [native-event (.-nativeEvent e)
|
||||
text (.-text native-event)
|
||||
content-size (.. native-event -contentSize)]
|
||||
(when (and (not single-line-input?)
|
||||
content-size)
|
||||
(set-layout-height-fn (.-height content-size)))
|
||||
(when (not= text input-text)
|
||||
(re-frame/dispatch [:set-chat-input-text text]))))
|
||||
:on-content-size-change (when (and (not input-focused?)
|
||||
(not single-line-input?))
|
||||
#(let [s (.-contentSize (.-nativeEvent %))
|
||||
w (.-width s)
|
||||
h (.-height s)]
|
||||
(set-container-width-fn w)
|
||||
(set-layout-height-fn h)))
|
||||
:on-selection-change #(let [s (-> (.-nativeEvent %)
|
||||
(.-selection))
|
||||
end (.-end s)]
|
||||
(re-frame/dispatch [:update-text-selection end]))
|
||||
:style (style/input-view single-line-input?)
|
||||
:placeholder-text-color colors/gray
|
||||
:auto-capitalize :sentences}
|
||||
(when cooldown-enabled?
|
||||
{:placeholder (i18n/label :cooldown/text-input-disabled)}))]))
|
||||
|
||||
(defview invisible-input [{:keys [set-layout-width-fn value]}]
|
||||
(letsubs [{:keys [input-text]} [:get-current-chat]]
|
||||
|
|
|
@ -249,6 +249,8 @@
|
|||
:counter-9-plus "9+"
|
||||
:show-more "Show more"
|
||||
:show-less "Show less"
|
||||
:cooldown/warning-message "Sorry, we limit sending several messages in quick succession to prevent spam. Please try again in a moment"
|
||||
:cooldown/text-input-disabled "Please wait a moment..."
|
||||
|
||||
;;discover
|
||||
:discover "Discover"
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
:my-profile/editing? false
|
||||
:transport/chats {}
|
||||
:transport/message-envelopes {}
|
||||
:chat/cooldowns 0
|
||||
:chat/cooldown-enabled? false
|
||||
:chat/last-outgoing-message-sent-at 0
|
||||
:chat/spam-messages-frequency 0
|
||||
:desktop/desktop {:tab-view-id :home}})
|
||||
|
||||
;;;;GLOBAL
|
||||
|
@ -187,6 +191,10 @@
|
|||
:browser/options
|
||||
:new/open-dapp
|
||||
:navigation/screen-params
|
||||
:chat/cooldowns
|
||||
:chat/cooldown-enabled?
|
||||
:chat/last-outgoing-message-sent-at
|
||||
:chat/spam-messages-frequency
|
||||
:transport/message-envelopes
|
||||
:transport/chats
|
||||
:transport/discovery-filter
|
||||
|
|
|
@ -44,3 +44,4 @@
|
|||
(def use-sym-key (enabled? (get-config :USE_SYM_KEY 0)))
|
||||
|
||||
(def group-chats-enabled? (enabled? (get-config :GROUP_CHATS_ENABLED)))
|
||||
(def spam-button-detection-enabled? (enabled? (get-config :SPAM_BUTTON_DETECTION_ENABLED "0")))
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
(ns status-im.test.chat.models.input
|
||||
(:require [cljs.test :refer-macros [deftest is]]
|
||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||
[status-im.chat.constants :as constants]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.utils.datetime :as datetime]
|
||||
[status-im.chat.models.input :as input]))
|
||||
|
||||
(def fake-db
|
||||
|
@ -166,3 +169,72 @@
|
|||
|
||||
(deftest modified-db-after-change
|
||||
"Just a combination of db modifications. Can be skipped now")
|
||||
|
||||
(deftest process-cooldown-fx
|
||||
(let [db {:current-chat-id "chat"
|
||||
:chats {"chat" {:public? true}}
|
||||
:chat/cooldowns 0
|
||||
:chat/spam-messages-frequency 0
|
||||
:chat/cooldown-enabled? false}]
|
||||
(with-redefs [datetime/timestamp (constantly 1527675198542)
|
||||
config/spam-button-detection-enabled? true]
|
||||
(testing "no spamming detected"
|
||||
(let [expected {:db (assoc db :chat/last-outgoing-message-sent-at 1527675198542)}
|
||||
actual (input/process-cooldown {:db db})]
|
||||
(is (= expected actual))))
|
||||
|
||||
(testing "spamming detected in 1-1"
|
||||
(let [db (assoc db
|
||||
:chats {"chat" {:public? false}}
|
||||
:chat/spam-messages-frequency constants/spam-message-frequency-threshold
|
||||
:chat/last-outgoing-message-sent-at (- 1527675198542 900))
|
||||
expected nil
|
||||
actual (input/process-cooldown {:db db})]
|
||||
(is (= expected actual))))
|
||||
|
||||
(testing "spamming detected"
|
||||
(let [db (assoc db
|
||||
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
|
||||
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
|
||||
expected {:db (assoc db
|
||||
:chat/last-outgoing-message-sent-at 1527675198542
|
||||
:chat/cooldowns 1
|
||||
:chat/spam-messages-frequency 0
|
||||
:chat/cooldown-enabled? true)
|
||||
:show-cooldown-warning nil
|
||||
:dispatch-later [{:dispatch [:disable-cooldown]
|
||||
:ms (constants/cooldown-periods-ms 1)}]}
|
||||
actual (input/process-cooldown {:db db})]
|
||||
(is (= expected actual))))
|
||||
|
||||
(testing "spamming detected twice"
|
||||
(let [db (assoc db
|
||||
:chat/cooldowns 1
|
||||
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
|
||||
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
|
||||
expected {:db (assoc db
|
||||
:chat/last-outgoing-message-sent-at 1527675198542
|
||||
:chat/cooldowns 2
|
||||
:chat/spam-messages-frequency 0
|
||||
:chat/cooldown-enabled? true)
|
||||
:show-cooldown-warning nil
|
||||
:dispatch-later [{:dispatch [:disable-cooldown]
|
||||
:ms (constants/cooldown-periods-ms 2)}]}
|
||||
actual (input/process-cooldown {:db db})]
|
||||
(is (= expected actual))))
|
||||
|
||||
(testing "spamming reaching cooldown threshold"
|
||||
(let [db (assoc db
|
||||
:chat/cooldowns (dec constants/cooldown-reset-threshold)
|
||||
:chat/last-outgoing-message-sent-at (- 1527675198542 900)
|
||||
:chat/spam-messages-frequency constants/spam-message-frequency-threshold)
|
||||
expected {:db (assoc db
|
||||
:chat/last-outgoing-message-sent-at 1527675198542
|
||||
:chat/cooldowns 0
|
||||
:chat/spam-messages-frequency 0
|
||||
:chat/cooldown-enabled? true)
|
||||
:show-cooldown-warning nil
|
||||
:dispatch-later [{:dispatch [:disable-cooldown]
|
||||
:ms (constants/cooldown-periods-ms 3)}]}
|
||||
actual (input/process-cooldown {:db db})]
|
||||
(is (= expected actual)))))))
|
||||
|
|
Loading…
Reference in New Issue