Prevent send button spamming in public chats

This commit is contained in:
Dmitry Novotochinov 2018-05-30 16:24:00 +03:00 committed by Roman Volosovskyi
parent 71ff9d9035
commit 00c5c630f4
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
15 changed files with 214 additions and 45 deletions

1
.env
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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})

View File

@ -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)))

View File

@ -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]

View File

@ -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))))))

View File

@ -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)

View File

@ -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

View File

@ -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?)))

View File

@ -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]]

View File

@ -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"

View File

@ -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

View File

@ -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")))

View File

@ -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)))))))