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
|
GROUP_CHATS_ENABLED=0
|
||||||
FORCE_SENT_RECEIVED_TRACKING=0
|
FORCE_SENT_RECEIVED_TRACKING=0
|
||||||
USE_SYM_KEY=0
|
USE_SYM_KEY=0
|
||||||
|
SPAM_BUTTON_DETECTION_ENABLED=1
|
|
@ -19,3 +19,4 @@ INSTABUG_SURVEYS=1
|
||||||
GROUP_CHATS_ENABLED=0
|
GROUP_CHATS_ENABLED=0
|
||||||
FORCE_SENT_RECEIVED_TRACKING=1
|
FORCE_SENT_RECEIVED_TRACKING=1
|
||||||
USE_SYM_KEY=0
|
USE_SYM_KEY=0
|
||||||
|
SPAM_BUTTON_DETECTION_ENABLED=1
|
||||||
|
|
|
@ -18,3 +18,4 @@ DEBUG_WEBVIEW=1
|
||||||
INSTABUG_SURVEYS=1
|
INSTABUG_SURVEYS=1
|
||||||
GROUP_CHATS_ENABLED=0
|
GROUP_CHATS_ENABLED=0
|
||||||
FORCE_SENT_RECEIVED_TRACKING=1
|
FORCE_SENT_RECEIVED_TRACKING=1
|
||||||
|
SPAM_BUTTON_DETECTION_ENABLED=1
|
||||||
|
|
|
@ -12,3 +12,12 @@
|
||||||
;; TODO(janherich): figure out something better then this
|
;; TODO(janherich): figure out something better then this
|
||||||
(def send-command-ref ["transactor" :command 83 "send"])
|
(def send-command-ref ["transactor" :command 83 "send"])
|
||||||
(def request-command-ref ["transactor" :command 83 "request"])
|
(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 :as handlers]
|
||||||
[status-im.utils.handlers-macro :as handlers-macro]
|
[status-im.utils.handlers-macro :as handlers-macro]
|
||||||
[status-im.utils.contacts :as utils.contacts]
|
[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.core :as transport.message]
|
||||||
[status-im.transport.message.v1.protocol :as protocol]
|
[status-im.transport.message.v1.protocol :as protocol]
|
||||||
[status-im.transport.message.v1.public-chat :as public-chat]
|
[status-im.transport.message.v1.public-chat :as public-chat]
|
||||||
|
@ -34,6 +35,13 @@
|
||||||
(fn [link]
|
(fn [link]
|
||||||
(list-selection/browse link)))
|
(list-selection/browse link)))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:show-cooldown-warning
|
||||||
|
(fn [_]
|
||||||
|
(utils/show-popup nil
|
||||||
|
(i18n/label :cooldown/warning-message)
|
||||||
|
#())))
|
||||||
|
|
||||||
;;;; Handlers
|
;;;; Handlers
|
||||||
|
|
||||||
(handlers/register-handler-db
|
(handlers/register-handler-db
|
||||||
|
@ -411,3 +419,9 @@
|
||||||
[re-frame/trim-v]
|
[re-frame/trim-v]
|
||||||
(fn [cofx [chat-id message-id]]
|
(fn [cofx [chat-id message-id]]
|
||||||
(models.message/delete-message chat-id message-id cofx)))
|
(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)
|
(and (multi-user-chat? chat-id cofx)
|
||||||
(not (get-in cofx [:db :chats chat-id :public?]))))
|
(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
|
(defn set-chat-ui-props
|
||||||
"Updates ui-props in active chat by merging provided kvs into them"
|
"Updates ui-props in active chat by merging provided kvs into them"
|
||||||
[{:keys [current-chat-id] :as db} kvs]
|
[{:keys [current-chat-id] :as db} kvs]
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[goog.object :as object]
|
[goog.object :as object]
|
||||||
[status-im.chat.constants :as const]
|
[status-im.chat.constants :as const]
|
||||||
|
[status-im.chat.models :as chat-model]
|
||||||
[status-im.chat.models.commands :as commands-model]
|
[status-im.chat.models.commands :as commands-model]
|
||||||
[status-im.js-dependencies :as dependencies]
|
[status-im.utils.config :as config]
|
||||||
[taoensso.timbre :as log]))
|
[status-im.utils.datetime :as datetime]
|
||||||
|
[status-im.js-dependencies :as dependencies]))
|
||||||
|
|
||||||
(defn text->emoji
|
(defn text->emoji
|
||||||
"Replaces emojis in a specified `text`"
|
"Replaces emojis in a specified `text`"
|
||||||
|
@ -198,3 +200,34 @@
|
||||||
[(keyword (get-in params [i :name])) value]))
|
[(keyword (get-in params [i :name])) value]))
|
||||||
(remove #(nil? (first %)))
|
(remove #(nil? (first %)))
|
||||||
(into {}))))
|
(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.events.requests :as requests-events]
|
||||||
[status-im.chat.models :as chat-model]
|
[status-im.chat.models :as chat-model]
|
||||||
[status-im.chat.models.commands :as commands-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.clocks :as utils.clocks]
|
||||||
[status-im.utils.handlers-macro :as handlers-macro]
|
[status-im.utils.handlers-macro :as handlers-macro]
|
||||||
[status-im.utils.money :as money]
|
[status-im.utils.money :as money]
|
||||||
|
@ -298,6 +299,7 @@
|
||||||
message-id (transport.utils/message-id send-record)
|
message-id (transport.utils/message-id send-record)
|
||||||
message-with-id (assoc message :message-id message-id)]
|
message-with-id (assoc message :message-id message-id)]
|
||||||
(handlers-macro/merge-fx cofx
|
(handlers-macro/merge-fx cofx
|
||||||
|
(input/process-cooldown)
|
||||||
(chat-model/upsert-chat {:chat-id chat-id
|
(chat-model/upsert-chat {:chat-id chat-id
|
||||||
:timestamp now})
|
:timestamp now})
|
||||||
(add-single-message message-with-id true)
|
(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/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/loaded-chats (s/nilable seq?))
|
||||||
(s/def :chat/bot-db (s/nilable map?))
|
(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?
|
:wallet-transaction-exists?
|
||||||
(fn [db [_ tx-hash]]
|
(fn [db [_ tx-hash]]
|
||||||
(not (nil? (get-in db [:wallet :transactions 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.send-button :as send-button]
|
||||||
[status-im.chat.views.input.suggestions :as suggestions]
|
[status-im.chat.views.input.suggestions :as suggestions]
|
||||||
[status-im.chat.views.input.validation-messages :as validation-messages]
|
[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.animation :as animation]
|
||||||
[status-im.ui.components.colors :as colors]
|
[status-im.ui.components.colors :as colors]
|
||||||
[status-im.ui.components.react :as react]
|
[status-im.ui.components.react :as react]
|
||||||
|
@ -27,15 +28,17 @@
|
||||||
(defview basic-text-input [{:keys [set-layout-height-fn set-container-width-fn height single-line-input?]}]
|
(defview basic-text-input [{:keys [set-layout-height-fn set-container-width-fn height single-line-input?]}]
|
||||||
(letsubs [{:keys [input-text]} [:get-current-chat]
|
(letsubs [{:keys [input-text]} [:get-current-chat]
|
||||||
input-focused? [:get-current-chat-ui-prop :input-focused?]
|
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
|
[react/text-input
|
||||||
|
(merge
|
||||||
{:ref #(when %
|
{:ref #(when %
|
||||||
(re-frame/dispatch [:set-chat-ui-props {:input-ref %}])
|
(re-frame/dispatch [:set-chat-ui-props {:input-ref %}])
|
||||||
(reset! input-ref %))
|
(reset! input-ref %))
|
||||||
:accessibility-label :chat-message-input
|
:accessibility-label :chat-message-input
|
||||||
:multiline (not single-line-input?)
|
:multiline (not single-line-input?)
|
||||||
:default-value (or input-text "")
|
:default-value (or input-text "")
|
||||||
:editable true
|
:editable (not cooldown-enabled?)
|
||||||
:blur-on-submit false
|
:blur-on-submit false
|
||||||
:on-focus #(re-frame/dispatch [:set-chat-ui-props {:input-focused? true
|
:on-focus #(re-frame/dispatch [:set-chat-ui-props {:input-focused? true
|
||||||
:messages-focused? false}])
|
:messages-focused? false}])
|
||||||
|
@ -69,7 +72,9 @@
|
||||||
(re-frame/dispatch [:update-text-selection end]))
|
(re-frame/dispatch [:update-text-selection end]))
|
||||||
:style (style/input-view single-line-input?)
|
:style (style/input-view single-line-input?)
|
||||||
:placeholder-text-color colors/gray
|
:placeholder-text-color colors/gray
|
||||||
:auto-capitalize :sentences}]))
|
:auto-capitalize :sentences}
|
||||||
|
(when cooldown-enabled?
|
||||||
|
{:placeholder (i18n/label :cooldown/text-input-disabled)}))]))
|
||||||
|
|
||||||
(defview invisible-input [{:keys [set-layout-width-fn value]}]
|
(defview invisible-input [{:keys [set-layout-width-fn value]}]
|
||||||
(letsubs [{:keys [input-text]} [:get-current-chat]]
|
(letsubs [{:keys [input-text]} [:get-current-chat]]
|
||||||
|
|
|
@ -249,6 +249,8 @@
|
||||||
:counter-9-plus "9+"
|
:counter-9-plus "9+"
|
||||||
:show-more "Show more"
|
:show-more "Show more"
|
||||||
:show-less "Show less"
|
: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 "Discover"
|
:discover "Discover"
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
:my-profile/editing? false
|
:my-profile/editing? false
|
||||||
:transport/chats {}
|
:transport/chats {}
|
||||||
:transport/message-envelopes {}
|
: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}})
|
:desktop/desktop {:tab-view-id :home}})
|
||||||
|
|
||||||
;;;;GLOBAL
|
;;;;GLOBAL
|
||||||
|
@ -187,6 +191,10 @@
|
||||||
:browser/options
|
:browser/options
|
||||||
:new/open-dapp
|
:new/open-dapp
|
||||||
:navigation/screen-params
|
:navigation/screen-params
|
||||||
|
:chat/cooldowns
|
||||||
|
:chat/cooldown-enabled?
|
||||||
|
:chat/last-outgoing-message-sent-at
|
||||||
|
:chat/spam-messages-frequency
|
||||||
:transport/message-envelopes
|
:transport/message-envelopes
|
||||||
:transport/chats
|
:transport/chats
|
||||||
:transport/discovery-filter
|
:transport/discovery-filter
|
||||||
|
|
|
@ -44,3 +44,4 @@
|
||||||
(def use-sym-key (enabled? (get-config :USE_SYM_KEY 0)))
|
(def use-sym-key (enabled? (get-config :USE_SYM_KEY 0)))
|
||||||
|
|
||||||
(def group-chats-enabled? (enabled? (get-config :GROUP_CHATS_ENABLED)))
|
(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
|
(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]))
|
[status-im.chat.models.input :as input]))
|
||||||
|
|
||||||
(def fake-db
|
(def fake-db
|
||||||
|
@ -166,3 +169,72 @@
|
||||||
|
|
||||||
(deftest modified-db-after-change
|
(deftest modified-db-after-change
|
||||||
"Just a combination of db modifications. Can be skipped now")
|
"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