Allow to edit group membership

Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
Andrea Maria Piana 2018-10-01 10:47:20 +02:00
parent 66ef32d3b9
commit 78b6d67d79
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
38 changed files with 932 additions and 374 deletions

View File

@ -1 +1 @@
0.16.0 0.16.1

View File

@ -694,8 +694,8 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
} }
@ReactMethod @ReactMethod
public void verifyGroupMembershipSignatures(final String signaturePairs, final Callback callback) { public void extractGroupMembershipSignatures(final String signaturePairs, final Callback callback) {
Log.d(TAG, "verifyGroupMembershipSignatures"); Log.d(TAG, "extractGroupMembershipSignatures");
if (!checkAvailability()) { if (!checkAvailability()) {
callback.invoke(false); callback.invoke(false);
return; return;
@ -704,7 +704,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
Runnable r = new Runnable() { Runnable r = new Runnable() {
@Override @Override
public void run() { public void run() {
String result = Statusgo.VerifyGroupMembershipSignatures(signaturePairs); String result = Statusgo.ExtractGroupMembershipSignatures(signaturePairs);
callback.invoke(result); callback.invoke(result);
} }

View File

@ -37,7 +37,8 @@ ExternalProject_Add(StatusGo_ep
PREFIX ${StatusGo_PREFIX} PREFIX ${StatusGo_PREFIX}
SOURCE_DIR ${StatusGo_SOURCE_DIR} SOURCE_DIR ${StatusGo_SOURCE_DIR}
GIT_REPOSITORY https://github.com/status-im/status-go.git GIT_REPOSITORY https://github.com/status-im/status-go.git
GIT_TAG f3880f8fe1f11e2cd59382c34dd826ebbf9662cf GIT_TAG 9f8f0089a3561e77b25279575928de8caba373cc
BUILD_BYPRODUCTS ${StatusGo_STATIC_LIB} BUILD_BYPRODUCTS ${StatusGo_STATIC_LIB}
CONFIGURE_COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/${CONFIGURE_SCRIPT} ${GO_ROOT_PATH} ${StatusGo_ROOT} ${StatusGo_SOURCE_DIR} CONFIGURE_COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/${CONFIGURE_SCRIPT} ${GO_ROOT_PATH} ${StatusGo_ROOT} ${StatusGo_SOURCE_DIR}
BUILD_COMMAND "" BUILD_COMMAND ""

View File

@ -203,12 +203,12 @@ void RCTStatus::signGroupMembership(QString content, double callbackId) {
}, content, callbackId); }, content, callbackId);
} }
void RCTStatus::verifyGroupMembershipSignatures(QString signatures, double callbackId) { void RCTStatus::extractGroupMembershipSignatures(QString signatures, double callbackId) {
Q_D(RCTStatus); Q_D(RCTStatus);
qDebug() << "call of RCTStatus::verifyGroupMembershipSignatures with param callbackId: " << callbackId; qDebug() << "call of RCTStatus::extractGroupMembershipSignatures with param callbackId: " << callbackId;
QtConcurrent::run([&](QString signatures, double callbackId) { QtConcurrent::run([&](QString signatures, double callbackId) {
const char* result = VerifyGroupMembershipSignatures(signatures.toUtf8().data()); const char* result = ExtractGroupMembershipSignatures(signatures.toUtf8().data());
qDebug() << "RCTStatus::verifyGroupMembershipSignatures VerifyGroupMembershipSignatures result: " << statusGoResultError(result); qDebug() << "RCTStatus::extractGroupMembershipSignatures ExtractGroupMembershipSignatures result: " << statusGoResultError(result);
d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); d->bridge->invokePromiseCallback(callbackId, QVariantList{result});
}, signatures, callbackId); }, signatures, callbackId);
} }

View File

@ -42,7 +42,7 @@ public:
Q_INVOKABLE void sendTransaction(QString txArgsJSON, QString password, double callbackId); Q_INVOKABLE void sendTransaction(QString txArgsJSON, QString password, double callbackId);
Q_INVOKABLE void signMessage(QString rpcParams, double callbackId); Q_INVOKABLE void signMessage(QString rpcParams, double callbackId);
Q_INVOKABLE void signGroupMembership(QString content, double callbackId); Q_INVOKABLE void signGroupMembership(QString content, double callbackId);
Q_INVOKABLE void verifyGroupMembershipSignatures(QString signatures, double callbackId); Q_INVOKABLE void extractGroupMembershipSignatures(QString signatures, double callbackId);
Q_INVOKABLE void setAdjustResize(); Q_INVOKABLE void setAdjustResize();
Q_INVOKABLE void setAdjustPan(); Q_INVOKABLE void setAdjustPan();

View File

@ -266,14 +266,14 @@ RCT_EXPORT_METHOD(signGroupMembership:(NSString *)content
} }
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
#pragma mark - VerifyGroupMembershipSignatures #pragma mark - ExtractGroupMembershipSignatures
//////////////////////////////////////////////////////////////////// verifyGroupMembershipSignatures //////////////////////////////////////////////////////////////////// extractGroupMembershipSignatures
RCT_EXPORT_METHOD(verifyGroupMembershipSignatures:(NSString *)content RCT_EXPORT_METHOD(extractGroupMembershipSignatures:(NSString *)content
callback:(RCTResponseSenderBlock)callback) { callback:(RCTResponseSenderBlock)callback) {
#if DEBUG #if DEBUG
NSLog(@"VerifyGroupMembershipSignatures() method called"); NSLog(@"ExtractGroupMembershipSignatures() method called");
#endif #endif
char * result = VerifyGroupMembershipSignatures((char *) [content UTF8String]); char * result = ExtractGroupMembershipSignatures((char *) [content UTF8String]);
callback(@[[NSString stringWithUTF8String: result]]); callback(@[[NSString stringWithUTF8String: result]]);
} }

View File

@ -25,7 +25,7 @@
(and (multi-user-chat? cofx chat-id) (and (multi-user-chat? cofx chat-id)
(not (get-in cofx [:db :chats chat-id :public?])))) (not (get-in cofx [:db :chats chat-id :public?]))))
(defn public-chat? [chat-id cofx] (defn public-chat? [cofx chat-id]
(get-in cofx [:db :chats chat-id :public?])) (get-in cofx [:db :chats chat-id :public?]))
(defn set-chat-ui-props (defn set-chat-ui-props
@ -47,7 +47,7 @@
:group-chat false :group-chat false
:is-active true :is-active true
:timestamp now :timestamp now
:contacts [chat-id] :contacts #{chat-id}
:last-clock-value 0})) :last-clock-value 0}))
(fx/defn upsert-chat (fx/defn upsert-chat
@ -58,11 +58,8 @@
(create-new-chat chat-id cofx)) (create-new-chat chat-id cofx))
chat-props)] chat-props)]
(if (:is-active chat) {:db (update-in db [:chats chat-id] merge chat)
{:db (update-in db [:chats chat-id] merge chat) :data-store/tx [(chats-store/save-chat-tx chat)]}))
:data-store/tx [(chats-store/save-chat-tx chat)]}
;; when chat is deleted, don't change anything
{:db db})))
(fx/defn add-public-chat (fx/defn add-public-chat
"Adds new public group chat to db & realm" "Adds new public group chat to db & realm"
@ -72,7 +69,7 @@
:is-active true :is-active true
:name topic :name topic
:group-chat true :group-chat true
:contacts [] :contacts #{}
:public? true})) :public? true}))
(fx/defn add-group-chat (fx/defn add-group-chat
@ -108,11 +105,8 @@
(messages-store/delete-messages-tx chat-id)]})) (messages-store/delete-messages-tx chat-id)]}))
(fx/defn remove-transport (fx/defn remove-transport
[{:keys [db] :as cofx} chat-id] [cofx chat-id]
;; if this is private group chat, we have to broadcast leave and unsubscribe after that (transport.utils/unsubscribe-from-chat cofx chat-id))
(if (group-chat? cofx chat-id)
(transport.message/send (transport/GroupLeave.) chat-id cofx)
(transport.utils/unsubscribe-from-chat cofx chat-id)))
(fx/defn deactivate-chat (fx/defn deactivate-chat
[{:keys [db now] :as cofx} chat-id] [{:keys [db now] :as cofx} chat-id]
@ -131,7 +125,7 @@
"Removes chat completely from app, producing all necessary effects for that" "Removes chat completely from app, producing all necessary effects for that"
[{:keys [db now] :as cofx} chat-id] [{:keys [db now] :as cofx} chat-id]
(fx/merge cofx (fx/merge cofx
#(when (multi-user-chat? % chat-id) #(when (public-chat? % chat-id)
(remove-transport % chat-id)) (remove-transport % chat-id))
(deactivate-chat chat-id) (deactivate-chat chat-id)
(clear-history chat-id) (clear-history chat-id)

View File

@ -1,55 +0,0 @@
(ns status-im.chat.models.group-chat
(:require [clojure.set :as set]
[clojure.string :as string]
[status-im.i18n :as i18n]
[status-im.transport.utils :as transport.utils]
[status-im.transport.message.core :as transport]
[status-im.transport.message.v1.core :as transport.message]
[status-im.ui.screens.group.core :as group]
[status-im.group-chats.core :as group-chat]
[status-im.chat.models :as models.chat]
[status-im.transport.message.core :as message]
[status-im.chat.models.message :as models.message]
[status-im.utils.fx :as fx]))
(defn- participants-diff [existing-participants-set new-participants-set]
{:removed (set/difference existing-participants-set new-participants-set)
:added (set/difference new-participants-set existing-participants-set)})
(defn- prepare-system-message [admin-name added-participants removed-participants contacts]
(let [added-participants-names (map #(get-in contacts [% :name] %) added-participants)
removed-participants-names (map #(get-in contacts [% :name] %) removed-participants)]
(cond
(and (seq added-participants) (seq removed-participants))
(str admin-name " "
(i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names))
" and "
(i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names)))
(seq added-participants)
(str admin-name " " (i18n/label :t/invited) " " (apply str (interpose ", " added-participants-names)))
(seq removed-participants)
(str admin-name " " (i18n/label :t/removed) " " (apply str (interpose ", " removed-participants-names))))))
(fx/defn handle-group-leave
[{:keys [db random-id-generator now] :as cofx} chat-id signature]
(let [me (:current-public-key db)
system-message-id (random-id-generator)
participant-leaving-name (or (get-in db [:contacts/contacts signature :name])
signature)]
(when (and
(not= signature me)
(get-in db [:chats chat-id])) ;; chat is present
(fx/merge cofx
#_(models.message/receive
(models.message/system-message chat-id random-id now
(str participant-leaving-name " " (i18n/label :t/left))))
(group/participants-removed chat-id #{signature})))))
(defn- group-name-from-contacts [selected-contacts all-contacts username]
(->> selected-contacts
(map (comp :name (partial get all-contacts)))
(cons username)
(string/join ", ")))

View File

@ -51,7 +51,7 @@
chat/cooldowns chat/cooldowns
chat/spam-messages-frequency chat/spam-messages-frequency
current-chat-id] :as db} :db :as cofx}] current-chat-id] :as db} :db :as cofx}]
(when (chat/public-chat? current-chat-id cofx) (when (chat/public-chat? cofx current-chat-id)
(let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at) (let [spamming-fast? (< (- (datetime/timestamp) last-outgoing-message-sent-at)
(+ chat.constants/spam-interval-ms (* 1000 cooldowns))) (+ chat.constants/spam-interval-ms (* 1000 cooldowns)))
spamming-frequently? (= chat.constants/spam-message-frequency-threshold spam-messages-frequency)] spamming-frequently? (= chat.constants/spam-message-frequency-threshold spam-messages-frequency)]

View File

@ -2,16 +2,61 @@
(:require [goog.object :as object] (:require [goog.object :as object]
[cljs.core.async :as async] [cljs.core.async :as async]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.utils.ethereum.core :as utils.ethereum]
[status-im.data-store.realm.core :as core])) [status-im.data-store.realm.core :as core]))
(defn remove-empty-vals
"Remove key/value when empty seq or nil"
[e]
(into {} (remove (fn [[_ v]]
(or (nil? v)
(and (coll? v)
(empty? v)))) e)))
(defn- event->string
"Transform an event in an a vector with keys in alphabetical order, to compute
a predictable id"
[event]
(js/JSON.stringify
(clj->js
(mapv
#(vector % (get event %))
(sort (keys event))))))
; Build an event id from a message
(def event-id (comp utils.ethereum/sha3 event->string))
(defn marshal-membership-updates [updates]
(mapcat (fn [{:keys [signature events from]}]
(map #(assoc %
:id (event-id %)
:signature signature
:from from) events)) updates))
(defn unmarshal-membership-updates [chat-id updates]
(->> updates
vals
(group-by :signature)
(map (fn [[signature events]]
{:events (map #(-> (dissoc % :signature :from :id)
remove-empty-vals) events)
:from (-> events first :from)
:signature signature
:chat-id chat-id}))))
(defn- get-last-clock-value [chat-id]
(-> (core/get-by-field @core/account-realm
:message :chat-id chat-id)
(core/sorted :clock-value :desc)
(core/single-clj :message)
:clock-value))
(defn- normalize-chat [{:keys [chat-id] :as chat}] (defn- normalize-chat [{:keys [chat-id] :as chat}]
(let [last-clock-value (-> (core/get-by-field @core/account-realm (let [last-clock-value (get-last-clock-value chat-id)]
:message :chat-id chat-id)
(core/sorted :clock-value :desc)
(core/single-clj :message)
:clock-value)]
(-> chat (-> chat
(update :admins #(into #{} %))
(update :contacts #(into #{} %)) (update :contacts #(into #{} %))
(update :membership-updates (partial unmarshal-membership-updates chat-id))
(assoc :last-clock-value (or last-clock-value 0))))) (assoc :last-clock-value (or last-clock-value 0)))))
(re-frame/reg-cofx (re-frame/reg-cofx
@ -27,7 +72,11 @@
"Returns tx function for saving chat" "Returns tx function for saving chat"
[{:keys [chat-id] :as chat}] [{:keys [chat-id] :as chat}]
(fn [realm] (fn [realm]
(core/create realm :chat chat true))) (core/create
realm
:chat
(update chat :membership-updates marshal-membership-updates)
true)))
;; Only used in debug mode ;; Only used in debug mode
(defn delete-chat-tx (defn delete-chat-tx

View File

@ -126,3 +126,34 @@
:default false} :default false}
:public? {:type :bool :public? {:type :bool
:default false}}}) :default false}}})
(def v7 {:name :chat
:primaryKey :chat-id
:properties {:chat-id :string
:name :string
:color {:type :string
:default default-chat-color}
:group-chat {:type :bool
:indexed true}
:is-active :bool
:timestamp :int
:contacts {:type "string[]"}
:admins {:type "string[]"}
:membership-updates {:type :list
:objectType :membership-update}
:removed-at {:type :int
:optional true}
:removed-from-at {:type :int
:optional true}
:deleted-at-clock-value {:type :int
:optional true}
:added-to-at {:type :int
:optional true}
:updated-at {:type :int
:optional true}
:message-overhead {:type :int
:default 0}
:debug? {:type :bool
:default false}
:public? {:type :bool
:default false}}})

View File

@ -9,6 +9,7 @@
[status-im.data-store.realm.schemas.account.browser :as browser] [status-im.data-store.realm.schemas.account.browser :as browser]
[status-im.data-store.realm.schemas.account.dapp-permissions :as dapp-permissions] [status-im.data-store.realm.schemas.account.dapp-permissions :as dapp-permissions]
[status-im.data-store.realm.schemas.account.request :as request] [status-im.data-store.realm.schemas.account.request :as request]
[status-im.data-store.realm.schemas.account.membership-update :as membership-update]
[status-im.data-store.realm.schemas.account.migrations :as migrations] [status-im.data-store.realm.schemas.account.migrations :as migrations]
[taoensso.timbre :as log])) [taoensso.timbre :as log]))
@ -152,6 +153,17 @@
browser/v8 browser/v8
dapp-permissions/v9]) dapp-permissions/v9])
(def v15 [chat/v7
transport/v6
contact/v1
message/v7
mailserver/v11
user-status/v1
membership-update/v1
local-storage/v1
browser/v8
dapp-permissions/v9])
;; put schemas ordered by version ;; put schemas ordered by version
(def schemas [{:schema v1 (def schemas [{:schema v1
:schemaVersion 1 :schemaVersion 1

View File

@ -0,0 +1,15 @@
(ns status-im.data-store.realm.schemas.account.membership-update)
(def v1 {:name :membership-update
:primaryKey :id
:properties {:id :string
:type :string
:name {:type :string
:optional true}
:clock-value :int
:signature :string
:from :string
:member {:type :string
:optional true}
:members {:type "string[]"
:optional true}}})

View File

@ -94,3 +94,6 @@
(.filtered (str "content-type = \"command-request\"")) (.filtered (str "content-type = \"command-request\""))
(.map (fn [message _ _] (.map (fn [message _ _]
(aset message "content-type" "command"))))) (aset message "content-type" "command")))))
(defn v15 [old-realm new-realm]
(log/debug "migrating v13 account database"))

View File

@ -10,7 +10,6 @@
[status-im.browser.core :as browser] [status-im.browser.core :as browser]
[status-im.browser.permissions :as browser.permissions] [status-im.browser.permissions :as browser.permissions]
[status-im.chat.models :as chat] [status-im.chat.models :as chat]
[status-im.chat.models.group-chat :as chat.group]
[status-im.chat.models.message :as chat.message] [status-im.chat.models.message :as chat.message]
[status-im.chat.models.loading :as chat.loading] [status-im.chat.models.loading :as chat.loading]
[status-im.chat.models.input :as chat.input] [status-im.chat.models.input :as chat.input]
@ -470,7 +469,7 @@
(handlers/register-handler-fx (handlers/register-handler-fx
:chat.ui/remove-chat-pressed :chat.ui/remove-chat-pressed
(fn [_ [_ chat-id group?]] (fn [_ [_ chat-id]]
{:ui/show-confirmation {:title (i18n/label :t/delete-confirmation) {:ui/show-confirmation {:title (i18n/label :t/delete-confirmation)
:content (i18n/label :t/delete-chat-confirmation) :content (i18n/label :t/delete-chat-confirmation)
:confirm-button-text (i18n/label :t/delete) :confirm-button-text (i18n/label :t/delete)
@ -903,6 +902,29 @@
(fn [cofx _] (fn [cofx _]
(group-chats/save cofx))) (group-chats/save cofx)))
(handlers/register-handler-fx
:group-chats.ui/add-members-pressed
(fn [cofx _]
(group-chats/add-members cofx)))
(handlers/register-handler-fx
:group-chats.ui/remove-member-pressed
(fn [cofx [_ chat-id public-key]]
(group-chats/remove-member cofx chat-id public-key)))
(handlers/register-handler-fx
:group-chats.ui/remove-chat-pressed
(fn [_ [_ chat-id group?]]
{:ui/show-confirmation {:title (i18n/label :t/delete-confirmation)
:content (i18n/label :t/delete-chat-confirmation)
:confirm-button-text (i18n/label :t/delete)
:on-accept #(re-frame/dispatch [:group-chats.ui/remove-chat-confirmed chat-id])}}))
(handlers/register-handler-fx
:group-chats.ui/remove-chat-confirmed
(fn [cofx [_ chat-id]]
(group-chats/remove cofx chat-id)))
(handlers/register-handler-fx (handlers/register-handler-fx
:group-chats.callback/sign-success :group-chats.callback/sign-success
[(re-frame/inject-cofx :random-guid-generator)] [(re-frame/inject-cofx :random-guid-generator)]
@ -910,6 +932,6 @@
(group-chats/handle-sign-success cofx group-update))) (group-chats/handle-sign-success cofx group-update)))
(handlers/register-handler-fx (handlers/register-handler-fx
:group-chats.callback/verify-signature-success :group-chats.callback/extract-signature-success
(fn [cofx [_ group-update sender-signature]] (fn [cofx [_ group-update sender-signature]]
(group-chats/handle-membership-update cofx group-update sender-signature))) (group-chats/handle-membership-update cofx group-update sender-signature)))

View File

@ -1,8 +1,12 @@
(ns status-im.group-chats.core (ns status-im.group-chats.core
(:refer-clojure :exclude [remove])
(:require [clojure.string :as string] (:require [clojure.string :as string]
[clojure.spec.alpha :as spec] [clojure.spec.alpha :as spec]
[clojure.set :as clojure.set]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.i18n :as i18n]
[status-im.utils.config :as config] [status-im.utils.config :as config]
[status-im.utils.clocks :as utils.clocks]
[status-im.native-module.core :as native-module] [status-im.native-module.core :as native-module]
[status-im.transport.utils :as transport.utils] [status-im.transport.utils :as transport.utils]
[status-im.transport.db :as transport.db] [status-im.transport.db :as transport.db]
@ -13,21 +17,54 @@
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.chat.models :as models.chat])) [status-im.chat.models :as models.chat]))
;; Description of the flow:
;; the flow is complicated a bit by 2 asynchronous call to status-go, which might make the logic a bit more opaque.
;; To send a group-membership update, we first build a message.
;; We then sign it with our private key and dispatch it.
;; Conversely when receiving a message, we first extract the public keys from the signature and attach those in the :from field.
;; We then process the events.
;; It is importatn that the from field is not trusted for updates coming from the outside.
;; When messages are coming from the database it can be trustured as already verified by us.
(defn sort-events [events]
(sort-by :clock-value events))
(defn- event->vector
"Transform an event in an a vector with keys in alphabetical order"
[event]
(mapv
#(vector % (get event %))
(sort (keys event))))
(defn get-last-clock-value
"Given a chat id get the last clock value of an event"
[cofx chat-id]
(->> (get-in cofx [:db :chats chat-id :membership-updates])
(mapcat :events)
(map :clock-value)
sort
last))
(defn- parse-response [response-js] (defn- parse-response [response-js]
(-> response-js (-> response-js
js/JSON.parse js/JSON.parse
(js->clj :keywordize-keys true))) (js->clj :keywordize-keys true)))
(defn signature-material [{:keys [chat-id admin participants]}] (defn signature-material
(apply str "Transform an update into a signable string"
(concat (sort participants) [chat-id events]
admin (js/JSON.stringify
chat-id))) (clj->js [(mapv event->vector (sort-events events)) chat-id])))
(defn signature-pairs [{:keys [admin signature] :as payload}] (defn signature-pairs
(js/JSON.stringify (clj->js [[(signature-material payload) "Transform a bunch of updates into signable pairs to be verified"
signature [{:keys [chat-id membership-updates] :as payload}]
(subs admin 2)]]))) (let [pairs (mapv (fn [{:keys [events signature]}]
[(signature-material chat-id events)
signature])
membership-updates)]
(js/JSON.stringify (clj->js pairs))))
(defn valid-chat-id? (defn valid-chat-id?
;; We need to make sure the chat-id ends with the admin pk (and it's not the same). ;; We need to make sure the chat-id ends with the admin pk (and it's not the same).
@ -39,103 +76,69 @@
(and (string/ends-with? chat-id admin) (and (string/ends-with? chat-id admin)
(not= chat-id admin))) (not= chat-id admin)))
(defn valid-event?
"Check if event can be applied to current group"
[{:keys [admins contacts]} {:keys [chat-id from member members] :as new-event}]
(when from
(condp = (:type new-event)
"chat-created" (and (empty? admins)
(empty? contacts))
"name-changed" (and (admins from)
(not (string/blank? (:name new-event))))
"members-added" (admins from)
"admins-added" (and (admins from)
(clojure.set/subset? members contacts))
"member-removed" (or
;; An admin removing a member
(and (admins from)
(not (admins member)))
;; Members can remove themselves
(and (not (admins member))
(contacts member)
(= from member)))
"admin-removed" (and (admins from)
(= from member)
(not= #{from} admins))
false)))
(defn wrap-group-message (defn wrap-group-message
"Wrap a group message in a membership update" "Wrap a group message in a membership update"
[cofx chat-id message] [cofx chat-id message]
(when-let [chat (get-in cofx [:db :chats chat-id])] (when-let [chat (get-in cofx [:db :chats chat-id])]
(transport/map->GroupMembershipUpdate. (transport/map->GroupMembershipUpdate.
{:chat-id chat-id {:chat-id chat-id
:chat-name (:name chat) :membership-updates (:membership-updates chat)
:admin (:group-admin chat) :message message})))
:participants (:contacts chat)
:signature (:membership-signature chat)
:version (:membership-version chat)
:message message})))
(fx/defn update-membership
"Upsert chat when version is greater or not existing"
[cofx previous-chat {:keys [chat-id
chat-name
participants
leaves
admin
signature
version]}]
(when
(or
(nil? previous-chat)
(< (:membership-version previous-chat)
version))
(models.chat/upsert-chat cofx
{:chat-id chat-id
:name chat-name
:is-active (get previous-chat :is-active true)
:group-chat true
:group-admin admin
:contacts participants
:membership-signature signature
:membership-version version})))
(defn send-membership-update (defn send-membership-update
"Send a membership update to all participants but the sender" "Send a membership update to all participants but the sender"
[cofx payload chat-id] ([cofx payload chat-id]
(let [{:keys [participants]} payload (send-membership-update cofx payload chat-id nil))
{:keys [current-public-key web3]} (:db cofx)] ([cofx payload chat-id removed-members]
(fx/merge (let [members (clojure.set/union (get-in cofx [:db :chats chat-id :contacts])
cofx removed-members)
{:shh/send-group-message {:web3 web3 {:keys [current-public-key web3]} (:db cofx)]
:src current-public-key (fx/merge
:dsts (disj participants current-public-key) cofx
:success-event [:transport/set-message-envelope-hash {:shh/send-group-message {:web3 web3
chat-id :src current-public-key
(transport.utils/message-id (:message payload)) :dsts (disj members current-public-key)
:group-user-message] :success-event [:transport/set-message-envelope-hash
:payload payload}}))) chat-id
(transport.utils/message-id (:message payload))
(defn send-group-leave [payload chat-id cofx] :group-user-message]
(transport.protocol/send cofx :payload payload}}))))
{:chat-id chat-id
:payload payload
:success-event [:group/unsubscribe-from-chat chat-id]}))
(fx/defn handle-membership-update-received (fx/defn handle-membership-update-received
"Verify signatures in status-go and act if successful" "Extract signatures in status-go and act if successful"
[cofx membership-update signature] [cofx membership-update signature]
{:group-chats/verify-membership-signature [[membership-update signature]]}) {:group-chats/extract-membership-signature [[membership-update signature]]})
(fx/defn handle-membership-update (defn chat->group-update
"Upsert chat and receive message if valid" "Transform a chat in a GroupMembershipUpdate"
;; Care needs to be taken here as chat-id is not coming from a whisper filter [chat-id {:keys [membership-updates]}]
;; so can be manipulated by the sending user. (transport/map->GroupMembershipUpdate. {:chat-id chat-id
[cofx {:keys [chat-id :membership-updates membership-updates}))
chat-name
participants
signature
leaves
message
admin
version] :as membership-update}
sender-signature]
(when (and config/group-chats-enabled?
(valid-chat-id? chat-id admin))
(let [previous-chat (get-in cofx [:db :chats chat-id])]
(fx/merge cofx
(update-membership previous-chat membership-update)
#(when (and message
;; don't allow anything but group messages
(instance? transport.protocol/Message message)
(= :group-user-message (:message-type message)))
(protocol.message/receive message chat-id sender-signature nil %))))))
(defn handle-sign-success
"Upsert chat and send signed payload to group members"
[{:keys [db] :as cofx} {:keys [chat-id] :as group-update}]
(let [my-public-key (:current-public-key db)]
(fx/merge cofx
(models.chat/navigate-to-chat chat-id {:navigation-reset? true})
(handle-membership-update group-update my-public-key)
#(protocol.message/send group-update chat-id %))))
(defn handle-sign-response (defn handle-sign-response
"Callback to dispatch on sign response" "Callback to dispatch on sign response"
@ -145,39 +148,87 @@
(re-frame/dispatch [:group-chats.callback/sign-failed error]) (re-frame/dispatch [:group-chats.callback/sign-failed error])
(re-frame/dispatch [:group-chats.callback/sign-success (assoc payload :signature signature)])))) (re-frame/dispatch [:group-chats.callback/sign-success (assoc payload :signature signature)]))))
(defn handle-verify-signature-response (defn add-identities
"Callback to dispatch on verify signature response" "Add verified identities extracted from the signature to the updates"
[payload sender-signature response-js] [payload identities]
(let [{:keys [error]} (parse-response response-js)] (update payload :membership-updates (fn [updates]
(if error (map
(re-frame/dispatch [:group-chats.callback/verify-signature-failed error]) #(assoc %1 :from (str "0x" %2))
(re-frame/dispatch [:group-chats.callback/verify-signature-success payload sender-signature])))) updates
identities))))
(defn sign-membership [payload] (defn handle-extract-signature-response
(native-module/sign-group-membership (signature-material payload) "Callback to dispatch on extract signature response"
[payload sender-signature response-js]
(let [{:keys [error identities]} (parse-response response-js)]
(if error
(re-frame/dispatch [:group-chats.callback/extract-signature-failed error])
(re-frame/dispatch [:group-chats.callback/extract-signature-success (add-identities payload identities) sender-signature]))))
(defn sign-membership [{:keys [chat-id events] :as payload}]
(native-module/sign-group-membership (signature-material chat-id events)
(partial handle-sign-response payload))) (partial handle-sign-response payload)))
(defn verify-membership-signature [signatures] (defn extract-membership-signature [payload sender]
(doseq [[payload sender-signature] signatures] (native-module/extract-group-membership-signatures (signature-pairs payload)
(native-module/verify-group-membership-signatures (signature-pairs payload) (partial handle-extract-signature-response payload sender)))
(partial handle-verify-signature-response payload sender-signature))))
(defn- members-added-event [last-clock-value members]
{:type "members-added"
:clock-value (utils.clocks/send last-clock-value)
:members members})
(fx/defn create (fx/defn create
"Format group update message and sign membership" "Format group update message and sign membership"
[{:keys [db random-guid-generator] :as cofx} group-name] [{:keys [db random-guid-generator] :as cofx} group-name]
(let [my-public-key (:current-public-key db) (let [my-public-key (:current-public-key db)
chat-id (str (random-guid-generator) my-public-key) chat-id (str (random-guid-generator) my-public-key)
selected-contacts (conj (:group/selected-contacts db) selected-contacts (:group/selected-contacts db)
my-public-key) clock-value (utils.clocks/send 0)
group-update (transport/map->GroupMembershipUpdate create-event {:type "chat-created"
{:chat-id chat-id :name group-name
:chat-name group-name :clock-value clock-value}
:admin my-public-key events [create-event
:participants selected-contacts (members-added-event clock-value selected-contacts)]]
:version 1})]
{:group-chats/sign-membership group-update {:group-chats/sign-membership {:chat-id chat-id
:from my-public-key
:events events}
:db (assoc db :group/selected-contacts #{})})) :db (assoc db :group/selected-contacts #{})}))
(fx/defn remove-member
"Format group update message and sign membership"
[{:keys [db] :as cofx} chat-id member]
(let [my-public-key (:current-public-key db)
last-clock-value (get-last-clock-value cofx chat-id)
chat (get-in cofx [:db :chats chat-id])
remove-event {:type "member-removed"
:member member
:clock-value (utils.clocks/send last-clock-value)}]
(when (valid-event? chat (assoc remove-event
:from
my-public-key))
{:group-chats/sign-membership {:chat-id chat-id
:from my-public-key
:events [remove-event]}})))
(fx/defn add-members
"Add members to a group chat"
[{{:keys [current-chat-id selected-participants current-public-key]} :db :as cofx}]
(let [last-clock-value (get-last-clock-value cofx current-chat-id)
events [(members-added-event last-clock-value selected-participants)]]
{:group-chats/sign-membership {:chat-id current-chat-id
:from current-public-key
:events events}}))
(fx/defn remove
"Remove & leave chat"
[{:keys [db] :as cofx} chat-id]
(let [my-public-key (:current-public-key db)]
(fx/merge cofx
(remove-member chat-id my-public-key)
(models.chat/remove-chat chat-id))))
(defn- valid-name? [name] (defn- valid-name? [name]
(spec/valid? :profile/name name)) (spec/valid? :profile/name name))
@ -201,11 +252,94 @@
{:db (assoc db :group-chat-profile/editing? false)} {:db (assoc db :group-chat-profile/editing? false)}
(models.chat/upsert-chat {:chat-id current-chat-id (models.chat/upsert-chat {:chat-id current-chat-id
:name new-name}))))) :name new-name})))))
(defn process-event
"Add/remove an event to a group"
[group {:keys [type member members chat-id from name] :as event}]
(if (valid-event? group event)
(case type
"chat-created" {:name name
:admins #{from}
:contacts #{from}}
"name-changed" (assoc group :name name)
"members-added" (update group :contacts clojure.set/union (into #{} members))
"admins-added" (update group :admins clojure.set/union (into #{} members))
"member-removed" (update group :contacts disj member)
"admin-removed" (update group :admins disj member))
group))
(defn build-group
"Given a list of already authenticated events build a group with contats/admin"
[events]
(->> events
sort-events
(reduce
process-event
{:admins #{}
:contacts #{}})))
(fx/defn update-membership
"Upsert chat when version is greater or not existing"
[cofx previous-chat {:keys [chat-id] :as new-chat}]
(let [all-updates (clojure.set/union (into #{} (:membership-updates previous-chat))
(into #{} (:membership-updates new-chat)))
unwrapped-events (mapcat
(fn [{:keys [events from]}]
(map #(assoc % :from from) events))
all-updates)
new-group (build-group unwrapped-events)]
(models.chat/upsert-chat cofx
{:chat-id chat-id
:name (:name new-group)
:is-active (get previous-chat :is-active true)
:group-chat true
:membership-updates (into [] all-updates)
:admins (:admins new-group)
:contacts (:contacts new-group)})))
(fx/defn handle-membership-update
"Upsert chat and receive message if valid"
;; Care needs to be taken here as chat-id is not coming from a whisper filter
;; so can be manipulated by the sending user.
[cofx {:keys [chat-id
message
membership-updates] :as membership-update}
sender-signature]
(when (and config/group-chats-enabled?
(valid-chat-id? chat-id (-> membership-updates first :from)))
(let [previous-chat (get-in cofx [:db :chats chat-id])]
(fx/merge cofx
(update-membership previous-chat membership-update)
#(when (and message
;; don't allow anything but group messages
(instance? transport.protocol/Message message)
(= :group-user-message (:message-type message)))
(protocol.message/receive message chat-id sender-signature nil %))))))
(defn handle-sign-success
"Upsert chat and send signed payload to group members"
[{:keys [db] :as cofx} {:keys [chat-id] :as signed-events}]
(let [old-chat (get-in db [:chats chat-id])
updated-chat (update old-chat :membership-updates conj signed-events)
my-public-key (:current-public-key db)
group-update (chat->group-update chat-id updated-chat)
new-group-fx (handle-membership-update group-update my-public-key)
;; We need to send to users who have been removed as well
recipients (clojure.set/union
(:contacts old-chat)
(get-in new-group-fx [:db :chats chat-id :contacts]))]
(fx/merge cofx
new-group-fx
#(when (get-in % [:db :chats chat-id :is-active])
(models.chat/navigate-to-chat % chat-id {:navigation-reset? true}))
#(send-membership-update % group-update chat-id recipients))))
(re-frame/reg-fx (re-frame/reg-fx
:group-chats/sign-membership :group-chats/sign-membership
sign-membership) sign-membership)
(re-frame/reg-fx (re-frame/reg-fx
:group-chats/verify-membership-signature :group-chats/extract-membership-signature
(fn [signatures] (fn [signatures]
(verify-membership-signature signatures))) (doseq [[payload sender] signatures]
(extract-membership-signature payload sender))))

View File

@ -60,6 +60,6 @@
(defn is24Hour [] (defn is24Hour []
(native-module/is24Hour)) (native-module/is24Hour))
(def verify-group-membership-signatures native-module/verify-group-membership-signatures) (def extract-group-membership-signatures native-module/extract-group-membership-signatures)
(def sign-group-membership native-module/sign-group-membership) (def sign-group-membership native-module/sign-group-membership)

View File

@ -134,9 +134,9 @@
(fn [UUID] (fn [UUID]
(callback (string/upper-case UUID)))))) (callback (string/upper-case UUID))))))
(defn verify-group-membership-signatures [signature-pairs callback] (defn extract-group-membership-signatures [signature-pairs callback]
(when status (when status
(call-module #(.verifyGroupMembershipSignatures status signature-pairs callback)))) (call-module #(.extractGroupMembershipSignatures status signature-pairs callback))))
(defn sign-group-membership [content callback] (defn sign-group-membership [content callback]
(when status (when status

View File

@ -43,8 +43,15 @@
(spec/def :chat/name (spec/nilable string?)) (spec/def :chat/name (spec/nilable string?))
(spec/def :group-chat/admin :global/public-key) (spec/def :group-chat/admin :global/public-key)
(spec/def :group-chat/participants (spec/coll-of :global/public-key :kind set?)) (spec/def :group-chat/signature :global/not-empty-string)
(spec/def :group-chat/signature string?) (spec/def :group-chat/chat-id :global/not-empty-string)
(spec/def :group-chat/type :global/not-empty-string)
(spec/def :group-chat/member :global/not-empty-string)
(spec/def :group-chat/name :global/not-empty-string)
(spec/def :group-chat/event (spec/keys :req-un [::clock-value :group-chat/type] :opt-un [:group-chat/member :group-chat/name]))
(spec/def :group-chat/events (spec/coll-of :group-chat/event))
(spec/def :group-chat/membership-updates (spec/coll-of (spec/keys :req-un [:group-chat/signature :group-chat/events])))
(spec/def :message.content/text (spec/and string? (complement s/blank?))) (spec/def :message.content/text (spec/and string? (complement s/blank?)))
(spec/def :message.content/response-to string?) (spec/def :message.content/response-to string?)
@ -73,7 +80,7 @@
(spec/def :message/message-seen (spec/keys :req-un [:message/ids])) (spec/def :message/message-seen (spec/keys :req-un [:message/ids]))
(spec/def :message/group-membership-update (spec/keys :req-un [:chat/chat-id :chat/name :group-chat/admin :group-chat/participants :group-chat/signature :message/message])) (spec/def :message/group-membership-update (spec/keys :req-un [:group-chat/membership-updates :group-chat/chat-id]))
(spec/def :message/message-common (spec/keys :req-un [::content-type ::message-type ::clock-value ::timestamp])) (spec/def :message/message-common (spec/keys :req-un [::content-type ::message-type ::clock-value ::timestamp]))
(spec/def :message.text/content (spec/keys :req-un [:message.content/text] (spec/def :message.text/content (spec/keys :req-un [:message.content/text]

View File

@ -1,6 +1,5 @@
(ns status-im.transport.impl.receive (ns status-im.transport.impl.receive
(:require (:require
[status-im.chat.models.group-chat :as models.group-chat]
[status-im.models.contact :as models.contact] [status-im.models.contact :as models.contact]
[status-im.group-chats.core :as group-chats] [status-im.group-chats.core :as group-chats]
[status-im.transport.message.core :as message] [status-im.transport.message.core :as message]
@ -12,11 +11,6 @@
(receive [this _ signature _ cofx] (receive [this _ signature _ cofx]
(group-chats/handle-membership-update-received cofx this signature))) (group-chats/handle-membership-update-received cofx this signature)))
(extend-type transport.protocol/GroupLeave
message/StatusMessage
(receive [this chat-id signature _ cofx]
(models.group-chat/handle-group-leave cofx chat-id signature)))
(extend-type transport.contact/ContactRequest (extend-type transport.contact/ContactRequest
message/StatusMessage message/StatusMessage
(receive [this _ signature timestamp cofx] (receive [this _ signature timestamp cofx]

View File

@ -8,8 +8,3 @@
message/StatusMessage message/StatusMessage
(send [this chat-id cofx] (send [this chat-id cofx]
(group-chats/send-membership-update cofx this chat-id))) (group-chats/send-membership-update cofx this chat-id)))
(extend-type transport/GroupLeave
message/StatusMessage
(send [this chat-id cofx]
(group-chats/send-group-leave this chat-id cofx)))

View File

@ -81,17 +81,11 @@
(rep [this {:keys [message-ids]}] (rep [this {:keys [message-ids]}]
(clj->js message-ids))) (clj->js message-ids)))
(deftype GroupLeaveHandler []
Object
(tag [this v] "g3")
(rep [this _]
(clj->js nil)))
(deftype GroupMembershipUpdateHandler [] (deftype GroupMembershipUpdateHandler []
Object Object
(tag [this v] "g5") (tag [this v] "g5")
(rep [this {:keys [chat-id chat-name admin participants leaves version signature message]}] (rep [this {:keys [chat-id membership-updates message]}]
#js [chat-id chat-name admin participants leaves version signature message])) #js [chat-id membership-updates message]))
(def writer (transit/writer :json (def writer (transit/writer :json
{:handlers {:handlers
@ -101,7 +95,6 @@
v1.contact/ContactUpdate (ContactUpdateHandler.) v1.contact/ContactUpdate (ContactUpdateHandler.)
v1.protocol/Message (MessageHandler.) v1.protocol/Message (MessageHandler.)
v1.protocol/MessagesSeen (MessagesSeenHandler.) v1.protocol/MessagesSeen (MessagesSeenHandler.)
v1/GroupLeave (GroupLeaveHandler.)
v1/GroupMembershipUpdate (GroupMembershipUpdateHandler.)}})) v1/GroupMembershipUpdate (GroupMembershipUpdateHandler.)}}))
;; ;;
@ -152,8 +145,8 @@
(v1.protocol/MessagesSeen. message-ids)) (v1.protocol/MessagesSeen. message-ids))
"c6" (fn [[name profile-image address fcm-token]] "c6" (fn [[name profile-image address fcm-token]]
(v1.contact/ContactUpdate. name profile-image address fcm-token)) (v1.contact/ContactUpdate. name profile-image address fcm-token))
"g5" (fn [[chat-id chat-name admin participants leaves version signature message]] "g5" (fn [[chat-id membership-updates message]]
(v1/GroupMembershipUpdate. chat-id chat-name admin participants leaves version signature message))}})) ; removed group chat handlers for https://github.com/status-im/status-react/issues/4506 (v1/GroupMembershipUpdate. chat-id membership-updates message))}}))
(defn serialize (defn serialize
"Serializes a record implementing the StatusMessage protocol using the custom writers" "Serializes a record implementing the StatusMessage protocol using the custom writers"

View File

@ -1,15 +1,12 @@
(ns status-im.transport.message.v1.core (ns status-im.transport.message.v1.core
(:require [status-im.transport.message.core :as message] (:require [status-im.transport.message.core :as message]
[taoensso.timbre :as log]
[cljs.spec.alpha :as spec])) [cljs.spec.alpha :as spec]))
(defrecord GroupMembershipUpdate (defrecord GroupMembershipUpdate
[chat-id chat-name admin participants leaves version signature message] [chat-id membership-updates message]
message/StatusMessage message/StatusMessage
(validate [this] (validate [this]
(when (spec/valid? :message/group-membership-update this) (if (spec/valid? :message/group-membership-update this)
this))) this
(log/warn "failed group membership validation" (spec/explain :message/group-membership-update this)))))
(defrecord GroupLeave
[]
message/StatusMessage
(validate [this] this))

View File

@ -1,6 +1,7 @@
(ns status-im.ui.components.contact.contact (ns status-im.ui.components.contact.contact
(:require-macros [status-im.utils.views :as views]) (:require-macros [status-im.utils.views :as views])
(:require [status-im.i18n :as i18n] (:require [status-im.i18n :as i18n]
[status-im.utils.platform :as platform]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.components.chat-icon.screen :as chat-icon]
@ -9,6 +10,15 @@
[status-im.ui.components.list.views :as list] [status-im.ui.components.list.views :as list]
[status-im.utils.gfycat.core :as gfycat])) [status-im.utils.gfycat.core :as gfycat]))
(defn desktop-extended-options [options]
[react/view {}
(doall (for [{:keys [label action]} options]
^{:key label}
[react/touchable-highlight
{:on-press action}
[react/view {}
[react/text {} label]]]))])
(defn- contact-inner-view (defn- contact-inner-view
([{:keys [info style props] {:keys [whisper-identity name dapp?] :as contact} :contact}] ([{:keys [info style props] {:keys [whisper-identity name dapp?] :as contact} :contact}]
[react/view (merge styles/contact-inner-container style) [react/view (merge styles/contact-inner-container style)
@ -39,12 +49,14 @@
[react/view styles/forward-btn [react/view styles/forward-btn
[vector-icons/icon :icons/forward]]) [vector-icons/icon :icons/forward]])
(when (and extended? (not (empty? extend-options))) (when (and extended? (not (empty? extend-options)))
[react/view styles/more-btn-container (if platform/desktop?
[react/touchable-highlight {:on-press #(list-selection/show {:options extend-options (desktop-extended-options extend-options)
:title extend-title}) [react/view styles/more-btn-container
:accessibility-label :menu-option} [react/touchable-highlight {:on-press #(list-selection/show {:options extend-options
[react/view styles/more-btn :title extend-title})
[vector-icons/icon :icons/options {:accessibility-label :options}]]]])]]) :accessibility-label :menu-option}
[react/view styles/more-btn
[vector-icons/icon :icons/options {:accessibility-label :options}]]]]))]])
(views/defview toogle-contact-view [{:keys [whisper-identity] :as contact} selected-key on-toggle-handler] (views/defview toogle-contact-view [{:keys [whisper-identity] :as contact} selected-key on-toggle-handler]
(views/letsubs [checked [selected-key whisper-identity]] (views/letsubs [checked [selected-key whisper-identity]]

View File

@ -28,7 +28,10 @@
(defn- delete-chat [chat-id group?] (defn- delete-chat [chat-id group?]
{:label (i18n/label :t/delete-chat) {:label (i18n/label :t/delete-chat)
:action #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id group?])}) :action #(re-frame/dispatch [(if group?
:group-chats.ui/remove-chat-pressed
:chat.ui/remove-chat-pressed)
chat-id])})
(defn- chat-actions [chat-id] (defn- chat-actions [chat-id]
[view-my-wallet [view-my-wallet

View File

@ -93,8 +93,8 @@
(:name current-account) (:name current-account)
(:name (contacts identity)))))) (:name (contacts identity))))))
(defn query-chat-contacts [[{:keys [contacts group-admin]} all-contacts] [_ query-fn]] (defn query-chat-contacts [[{:keys [contacts]} all-contacts] [_ query-fn]]
(let [participant-set (into #{} (filter identity) (conj contacts group-admin))] (let [participant-set (into #{} (filter identity) contacts)]
(query-fn (comp participant-set :whisper-identity) (vals all-contacts)))) (query-fn (comp participant-set :whisper-identity) (vals all-contacts))))
(reg-sub :query-current-chat-contacts (reg-sub :query-current-chat-contacts
@ -104,24 +104,26 @@
(reg-sub :get-all-contacts-not-in-current-chat (reg-sub :get-all-contacts-not-in-current-chat
:<- [:query-current-chat-contacts remove] :<- [:query-current-chat-contacts remove]
identity) (fn [contacts]
(remove :dapp? contacts)))
(defn get-all-contacts-in-group-chat [chat-contact-ids group-admin-id contacts current-account] (defn get-all-contacts-in-group-chat [members contacts current-account]
(let [participant-set (into #{} (filter identity) (conj chat-contact-ids group-admin-id)) (let [current-account-contact (-> current-account
current-account-contact (-> current-account
(select-keys [:name :photo-path :public-key]) (select-keys [:name :photo-path :public-key])
(clojure.set/rename-keys {:public-key :whisper-identity})) (clojure.set/rename-keys {:public-key :whisper-identity}))
all-contacts (assoc contacts (:whisper-identity current-account-contact) current-account-contact)] all-contacts (assoc contacts (:whisper-identity current-account-contact) current-account-contact)]
(map #(or (get all-contacts %) (->> members
(utils.contacts/whisper-id->new-contact %)) (map #(or (get all-contacts %)
participant-set))) (utils.contacts/whisper-id->new-contact %)))
(remove :dapp?)
(sort-by (comp clojure.string/lower-case :name)))))
(reg-sub :get-current-chat-contacts (reg-sub :get-current-chat-contacts
:<- [:get-current-chat] :<- [:get-current-chat]
:<- [:get-contacts] :<- [:get-contacts]
:<- [:get-current-account] :<- [:get-current-account]
(fn [[{:keys [contacts group-admin]} all-contacts current-account]] (fn [[{:keys [contacts]} all-contacts current-account]]
(get-all-contacts-in-group-chat contacts group-admin all-contacts current-account))) (get-all-contacts-in-group-chat contacts all-contacts current-account)))
(reg-sub :get-contacts-by-chat (reg-sub :get-contacts-by-chat
(fn [[_ _ chat-id] _] (fn [[_ _ chat-id] _]

View File

@ -59,6 +59,10 @@
[react/text {:style (styles/profile-actions-text colors/black) [react/text {:style (styles/profile-actions-text colors/black)
:on-press #(re-frame/dispatch [:show-profile-desktop whisper-identity])} :on-press #(re-frame/dispatch [:show-profile-desktop whisper-identity])}
(i18n/label :t/view-profile)]) (i18n/label :t/view-profile)])
(when (and group-chat (not public?))
[react/text {:style (styles/profile-actions-text colors/black)
:on-press #(re-frame/dispatch [:show-group-chat-profile])}
(i18n/label :t/group-info)])
[react/text {:style (styles/profile-actions-text colors/black) [react/text {:style (styles/profile-actions-text colors/black)
:on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed])} :on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed])}
(i18n/label :t/clear-history)] (i18n/label :t/clear-history)]

View File

@ -5,7 +5,8 @@
[status-im.ui.screens.intro.views :as intro.views] [status-im.ui.screens.intro.views :as intro.views]
[status-im.ui.screens.group.add-contacts.views :refer [contact-toggle-list]] [status-im.ui.screens.group.add-contacts.views :refer [contact-toggle-list]]
[status-im.ui.screens.group.views :refer [new-group]] [status-im.ui.screens.group.views :refer [new-group]]
[status-im.ui.screens.profile.group-chat.views :refer [group-chat-profile]]
[status-im.ui.screens.group.add-contacts.views :refer [add-participants-toggle-list]]
[status-im.ui.screens.accounts.create.views :as create.views] [status-im.ui.screens.accounts.create.views :as create.views]
[status-im.ui.screens.accounts.login.views :as login.views] [status-im.ui.screens.accounts.login.views :as login.views]
[status-im.ui.screens.accounts.recover.views :as recover.views] [status-im.ui.screens.accounts.recover.views :as recover.views]
@ -22,6 +23,9 @@
:create-account create.views/create-account :create-account create.views/create-account
:new-group new-group :new-group new-group
:contact-toggle-list contact-toggle-list :contact-toggle-list contact-toggle-list
:group-chat-profile group-chat-profile
:add-participants-toggle-list add-participants-toggle-list
(:new-contact (:new-contact
:advanced-settings :advanced-settings
:chat :chat

View File

@ -44,13 +44,14 @@
(defview contact-toggle-list [] (defview contact-toggle-list []
(letsubs [contacts [:all-added-people-contacts] (letsubs [contacts [:all-added-people-contacts]
selected-contacts-count [:selected-contacts-count]] selected-contacts-count [:selected-contacts-count]]
[react/keyboard-avoiding-view {:style styles/group-container} (when (seq contacts)
[status-bar] [react/keyboard-avoiding-view {:style styles/group-container}
[toggle-list-toolbar {:handler #(re-frame/dispatch [:navigate-to :new-group]) [status-bar]
:label (i18n/label :t/next) [toggle-list-toolbar {:handler #(re-frame/dispatch [:navigate-to :new-group])
:count (pos? selected-contacts-count)} :label (i18n/label :t/next)
(i18n/label :t/group-chat)] :count (pos? selected-contacts-count)}
[toggle-list contacts group-toggle-contact]])) (i18n/label :t/group-chat)]
[toggle-list contacts group-toggle-contact]])))
;; Add participants to existing group chat ;; Add participants to existing group chat
(defview add-participants-toggle-list [] (defview add-participants-toggle-list []
@ -61,8 +62,9 @@
[status-bar] [status-bar]
[toggle-list-toolbar {:count selected-contacts-count [toggle-list-toolbar {:count selected-contacts-count
:handler #(do :handler #(do
(re-frame/dispatch [:add-new-group-chat-participants]) (re-frame/dispatch [:group-chats.ui/add-members-pressed])
(re-frame/dispatch [:navigate-back])) (re-frame/dispatch [:navigate-back]))
:label (i18n/label :t/add)} :label (i18n/label :t/add)}
name] name]
[toggle-list contacts group-toggle-participant]])) (when (seq contacts)
[toggle-list contacts group-toggle-participant])]))

View File

@ -1,22 +0,0 @@
(ns status-im.ui.screens.group.core
(:require [status-im.data-store.chats :as chats-store]
[status-im.utils.fx :as fx]))
(fx/defn participants-added
[{:keys [db] :as cofx} chat-id added-participants-set]
(when (seq added-participants-set)
{:db (update-in db [:chats chat-id :contacts]
concat added-participants-set)
:data-store/tx [(chats-store/add-chat-contacts-tx
chat-id added-participants-set)]}))
(fx/defn participants-removed
[{:keys [now db] :as cofx} chat-id removed-participants-set]
(when (seq removed-participants-set)
(let [{:keys [is-active timestamp]} (get-in db [:chats chat-id])]
;;TODO: not sure what this condition is for
(when (and is-active (>= now timestamp))
{:db (update-in db [:chats chat-id :contacts]
(partial remove removed-participants-set))
:data-store/tx [(chats-store/remove-chat-contacts-tx
chat-id removed-participants-set)]}))))

View File

@ -31,6 +31,8 @@
{:font-size 15 {:font-size 15
:text-align :center :text-align :center
:flex 1 :flex 1
:desktop {:height 20
:width 200}
:ios {:letter-spacing -0.2 :ios {:letter-spacing -0.2
:margin-top 1 :margin-top 1
:height 45 :height 45

View File

@ -1,6 +1,7 @@
(ns status-im.ui.screens.profile.group-chat.views (ns status-im.ui.screens.profile.group-chat.views
(:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [status-im.ui.screens.profile.group-chat.styles :as styles] (:require [status-im.utils.platform :as platform]
[status-im.ui.screens.profile.group-chat.styles :as styles]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.ui.screens.profile.components.styles :as profile.components.styles] [status-im.ui.screens.profile.components.styles :as profile.components.styles]
[status-im.ui.screens.profile.components.views :as profile.components] [status-im.ui.screens.profile.components.views :as profile.components]
@ -37,57 +38,59 @@
(defn actions [admin? chat-id] (defn actions [admin? chat-id]
(concat (concat
#_(when admin? (when admin?
[{:label (i18n/label :add-members) [{:label (i18n/label :add-members)
:icon :icons/add :icon :icons/add
:action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) :action #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}])
[{:label (i18n/label :t/clear-history) [{:label (i18n/label :t/clear-history)
:icon :icons/close :icon :icons/close
:action #(re-frame/dispatch [:chat.ui/clear-history-pressed]) :action #(re-frame/dispatch [:chat.ui/clear-history-pressed])
:accessibility-label :clear-history-button} :accessibility-label :clear-history-button}
{:label (i18n/label :t/delete-chat) {:label (i18n/label :t/delete-chat)
:icon :icons/arrow-left :icon :icons/arrow-left
:action #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id]) :action #(re-frame/dispatch [:group-chats.ui/remove-chat-pressed chat-id])
:accessibility-label :delete-chat-button}])) :accessibility-label :delete-chat-button}]))
(defn contact-actions [contact] (defn member-actions [chat-id member]
[{:action #(re-frame/dispatch [:chat.ui/show-profile (:whisper-identity contact)]) [{:action #(re-frame/dispatch [(if platform/desktop? :show-profile-desktop :chat.ui/show-profile) (:whisper-identity member)])
:label (i18n/label :t/view-profile)} :label (i18n/label :t/view-profile)}
#_{:action #(re-frame/dispatch [:remove-group-chat-participants #{(:whisper-identity contact)}]) {:action #(re-frame/dispatch [:group-chats.ui/remove-member-pressed chat-id (:whisper-identity member)])
:label (i18n/label :t/remove-from-chat)}]) :label (i18n/label :t/remove-from-chat)}])
(defn render-contact [{:keys [name whisper-identity] :as contact} admin? group-admin-identity current-user-identity] (defn render-member [chat-id {:keys [name whisper-identity] :as member} admin? current-user-identity]
[react/view [react/view
[contact/contact-view [contact/contact-view
{:contact contact {:contact member
:extend-options (contact-actions contact) :extend-options (member-actions chat-id member)
:extend-title name :extend-title name
:extended? (and admin? (not= whisper-identity group-admin-identity)) :extended? (and admin?
(not= whisper-identity current-user-identity))
:accessibility-label :member-item :accessibility-label :member-item
:inner-props {:accessibility-label :member-name-text} :inner-props {:accessibility-label :member-name-text}
:on-press (when (not= whisper-identity current-user-identity) :on-press (when (not= whisper-identity current-user-identity)
#(re-frame/dispatch [:chat.ui/show-profile whisper-identity]))}]]) #(re-frame/dispatch [(if platform/desktop? :show-profile-desktop :chat.ui/show-profile) whisper-identity]))}]])
(defview chat-group-contacts-view [admin? group-admin-identity current-user-identity] (defview chat-group-members-view [chat-id admin? current-user-identity]
(letsubs [contacts [:get-current-chat-contacts]] (letsubs [members [:get-current-chat-contacts]]
[react/view (when (seq members)
[list/flat-list {:data contacts [react/view
:separator list/default-separator [list/flat-list {:data members
:key-fn :address :separator list/default-separator
:render-fn #(render-contact % admin? group-admin-identity current-user-identity)}]])) :key-fn :address
:render-fn #(render-member chat-id % admin? current-user-identity)}]])))
(defn members-list [admin? group-admin-identity current-user-identity] (defn members-list [chat-id admin? current-user-identity]
[react/view [react/view
[profile.components/settings-title (i18n/label :t/members-title)] [profile.components/settings-title (i18n/label :t/members-title)]
[chat-group-contacts-view admin? group-admin-identity current-user-identity]]) [chat-group-members-view chat-id admin? current-user-identity]])
(defview group-chat-profile [] (defview group-chat-profile []
(letsubs [{:keys [group-admin] :as current-chat} [:get-current-chat] (letsubs [{:keys [admins chat-id] :as current-chat} [:get-current-chat]
editing? [:get :group-chat-profile/editing?] editing? [:get :group-chat-profile/editing?]
changed-chat [:get :group-chat-profile/profile] changed-chat [:get :group-chat-profile/profile]
current-pk [:get :current-public-key]] current-pk [:get :current-public-key]]
(let [shown-chat (merge current-chat changed-chat) (let [shown-chat (merge current-chat changed-chat)
admin? (= current-pk group-admin)] admin? (admins current-pk)]
[react/view profile.components.styles/profile [react/view profile.components.styles/profile
[status-bar/status-bar] [status-bar/status-bar]
(if editing? (if editing?
@ -100,10 +103,10 @@
:editing? editing? :editing? editing?
:allow-icon-change? false :allow-icon-change? false
:on-change-text-event :group-chats.ui/name-changed}] :on-change-text-event :group-chats.ui/name-changed}]
[list/action-list (actions admin? (:chat-id current-chat)) [list/action-list (actions admin? chat-id)
{:container-style styles/action-container {:container-style styles/action-container
:action-style styles/action :action-style styles/action
:action-label-style styles/action-label :action-label-style styles/action-label
:action-separator-style styles/action-separator :action-separator-style styles/action-separator
:icon-opts styles/action-icon-opts}] :icon-opts styles/action-icon-opts}]
[members-list admin? group-admin current-pk]]]]))) [members-list chat-id admin? (first admins) current-pk]]]])))

View File

@ -13,7 +13,7 @@
#{:data-store/tx :data-store/base-tx :chat-received-message/add-fx #{:data-store/tx :data-store/base-tx :chat-received-message/add-fx
:shh/add-new-sym-keys :shh/get-new-sym-keys :shh/post :shh/add-new-sym-keys :shh/get-new-sym-keys :shh/post
:shh/generate-sym-key-from-password :confirm-messages-processed :shh/generate-sym-key-from-password :confirm-messages-processed
:group-chats/verify-membership-signature :utils/dispatch-later}) :group-chats/extract-membership-signature :utils/dispatch-later})
(defn- safe-merge [fx new-fx] (defn- safe-merge [fx new-fx]
(if (:merging-fx-with-common-keys fx) (if (:merging-fx-with-common-keys fx)

View File

@ -46,20 +46,7 @@
(testing "it updates existins props" (testing "it updates existins props"
(is (= "new-name" (:name actual-chat)))) (is (= "new-name" (:name actual-chat))))
(testing "it adds the fx to store a chat" (testing "it adds the fx to store a chat"
(is store-chat-fx)))) (is store-chat-fx)))))
(testing "upserting a deleted chat"
(let [chat-id "some-chat-id"
contact-name "contact-name"
chat-props {:chat-id chat-id
:name "new-name"
:extra-prop "some"}
cofx {:some-cofx "b"
:db {:chats {chat-id {:is-active false
:name "old-name"}}}}]
(testing "it updates it if is-active is passed"
(is (get-in (chat/upsert-chat cofx (assoc chat-props :is-active true)) [:db :chats chat-id :is-active])))
(testing "it returns the db unchanged"
(is (= {:db (:db cofx)} (chat/upsert-chat cofx chat-props)))))))
(deftest add-group-chat (deftest add-group-chat
(let [chat-id "chat-id" (let [chat-id "chat-id"
@ -94,7 +81,7 @@
(testing "it sets the name" (testing "it sets the name"
(is (= topic (:name chat)))) (is (= topic (:name chat))))
(testing "it sets the participants" (testing "it sets the participants"
(is (= [] (:contacts chat)))) (is (= #{} (:contacts chat))))
(testing "it sets the chat-id" (testing "it sets the chat-id"
(is (= topic (:chat-id chat)))) (is (= topic (:chat-id chat))))
(testing "it sets the group-chat flag" (testing "it sets the group-chat flag"

View File

@ -7,8 +7,8 @@
(testing "get-all-contacts-in-group-chat" (testing "get-all-contacts-in-group-chat"
(with-redefs [identicon/identicon (constantly "generated")] (with-redefs [identicon/identicon (constantly "generated")]
(let [chat-contact-ids ["0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917" (let [chat-contact-ids ["0x04fcf40c526b09ff9fb22f4a5dbd08490ef9b64af700870f8a0ba2133f4251d5607ed83cd9047b8c2796576bc83fa0de23a13a4dced07654b8ff137fe744047917"
"0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f"
"0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"] "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"]
group-admin-id "0x04985040682b77a32bb4bb58268a0719bd24ca4d07c255153fe1eb2ccd5883669627bd1a092d7cc76e8e4b9104327667b19dcda3ac469f572efabe588c38c1985f"
contacts {"demo-bot" contacts {"demo-bot"
{:description nil, {:description nil,
:last-updated 0, :last-updated 0,
@ -76,7 +76,6 @@
:public-key :public-key
"0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"}] "0x048a2f8b80c60f89a91b4c1316e56f75b087f446e7b8701ceca06a40142d8efe1f5aa36bd0fee9e248060a8d5207b43ae98bef4617c18c71e66f920f324869c09f"}]
(is (= (contacts-subs/get-all-contacts-in-group-chat chat-contact-ids (is (= (contacts-subs/get-all-contacts-in-group-chat chat-contact-ids
group-admin-id
contacts contacts
current-account) current-account)
[{:name "Snappy Impressive Leonberger" [{:name "Snappy Impressive Leonberger"

View File

@ -0,0 +1,50 @@
(ns status-im.test.data-store.chats
(:require [cljs.test :refer-macros [deftest is testing]]
[status-im.utils.random :as utils.random]
[status-im.data-store.chats :as chats]))
(deftest normalize-chat-test
(testing "admins & contacts"
(with-redefs [chats/get-last-clock-value (constantly 42)]
(is (= {:last-clock-value 42
:admins #{4}
:contacts #{2}
:membership-updates []}
(chats/normalize-chat {:admins [4]
:contacts [2]})))))
(testing "membership-updates"
(with-redefs [chats/get-last-clock-value (constantly 42)]
(let [raw-events {"1" {:id "1" :type "members-added" :clock-value 10 :members [1 2] :signature "a" :from "id-1"}
"2" {:id "2" :type "member-removed" :clock-value 11 :member 1 :signature "a" :from "id-1"}
"3" {:id "3" :type "chat-created" :clock-value 0 :name "blah" :signature "b" :from "id-2"}}
expected #{{:chat-id "chat-id"
:from "id-2"
:signature "b"
:events [{:type "chat-created" :clock-value 0 :name "blah"}]}
{:chat-id "chat-id"
:signature "a"
:from "id-1"
:events [{:type "members-added" :clock-value 10 :members [1 2]}
{:type "member-removed" :clock-value 11 :member 1}]}}
actual (->> (chats/normalize-chat {:chat-id "chat-id"
:membership-updates raw-events})
:membership-updates
(into #{}))]
(is (= expected
actual))))))
(deftest marshal-membership-updates-test
(let [raw-updates [{:chat-id "chat-id"
:signature "b"
:from "id-1"
:events [{:type "chat-created" :clock-value 0 :name "blah"}]}
{:chat-id "chat-id"
:signature "a"
:from "id-2"
:events [{:type "members-added" :clock-value 10 :members [1 2]}
{:type "member-removed" :clock-value 11 :member 1}]}]
expected #{{:type "members-added" :clock-value 10 :from "id-2" :members [1 2] :signature "a" :id "0xb7690375de21da4890d2d5acca8b56e327d9eb75fd3b4bcceca4bf1679c2f830"}
{:type "member-removed" :clock-value 11 :from "id-2" :member 1 :signature "a" :id "0x2a66f195abf6e6903c4245e372e1e2e6aea2b2c0a74ad03080a313e94197a64f"}
{:type "chat-created" :clock-value 0 :from "id-1" :name "blah" :signature "b" :id "0x7fad22accf1dec64daedf83e7af19b0dcde8c5facfb479874a48da5fb6967e07"}}
actual (into #{} (chats/marshal-membership-updates raw-updates))]
(is (= expected actual))))

View File

@ -1,5 +1,6 @@
(ns status-im.test.group-chats.core (ns status-im.test.group-chats.core
(:require [cljs.test :refer-macros [deftest is testing]] (:require [cljs.test :refer-macros [deftest is testing]]
[status-im.utils.clocks :as utils.clocks]
[status-im.utils.config :as config] [status-im.utils.config :as config]
[status-im.group-chats.core :as group-chats])) [status-im.group-chats.core :as group-chats]))
@ -8,24 +9,24 @@
(def member-1 "member-1") (def member-1 "member-1")
(def member-2 "member-2") (def member-2 "member-2")
(def member-3 "member-3")
(def member-4 "member-4")
(def admin member-1) (def admin member-1)
(def chat-id (str random-id admin)) (def chat-id (str random-id admin))
(def invitation-m1 {:id "m-1"
:user member-1})
(def invitation-m2 {:id "m-2"
:user member-2})
(def initial-message {:chat-id chat-id (def initial-message {:chat-id chat-id
:chat-name chat-name :membership-updates [{:from admin
:admin admin :events [{:type "chat-created"
:participants [invitation-m1 :name "chat-name"
invitation-m2] :clock-value 1}
:leaves [] {:type "members-added"
:signature "some" :clock-value 3
:version 1}) :members [member-2 member-3]}]}]})
(deftest get-last-clock-value-test
(is (= 3 (group-chats/get-last-clock-value {:db {:chats {chat-id initial-message}}} chat-id))))
(deftest handle-group-membership-update (deftest handle-group-membership-update
(with-redefs [config/group-chats-enabled? true] (with-redefs [config/group-chats-enabled? true]
@ -37,13 +38,21 @@
(get chat-id))] (get chat-id))]
(testing "it creates a new chat" (testing "it creates a new chat"
(is actual)) (is actual))
(testing "it sets the right chat-name"
(is (= "chat-name"
(:name actual))))
(testing "it sets the right chat-id"
(is (= chat-id
(:chat-id actual))))
(testing "it sets the right participants" (testing "it sets the right participants"
(is (= [invitation-m1 (is (= #{member-1 member-2 member-3}
invitation-m2]
(:contacts actual)))) (:contacts actual))))
(testing "it sets the right version" (testing "it sets the updates"
(is (= 1 (is (= (:membership-updates initial-message)
(:membership-version actual)))))) (:membership-updates actual))))
(testing "it sets the right admins"
(is (= #{admin}
(:admins actual))))))
(testing "a chat with the wrong id" (testing "a chat with the wrong id"
(let [bad-chat-id (str random-id member-2) (let [bad-chat-id (str random-id member-2)
actual (-> actual (->
@ -57,16 +66,325 @@
(testing "it does not create a chat" (testing "it does not create a chat"
(is (not actual))))) (is (not actual)))))
(testing "an already existing chat" (testing "an already existing chat"
(let [cofx {:db {:chats {chat-id {:contacts [invitation-m1 (let [cofx {:db {:chats {chat-id {:admins #{admin}
invitation-m2] :name "chat-name"
:group-admin admin :chat-id chat-id
:membership-version 2}}}}] :is-active true
(testing "an update from the admin is received" :group-chat true
(testing "the message is an older version" :contacts #{member-1 member-2 member-3}
(let [actual (group-chats/handle-membership-update cofx initial-message admin)] :membership-updates (:membership-updates initial-message)}}}}]
(testing "it noops" (testing "the message has already been received"
(is (= actual cofx))))) (let [actual (group-chats/handle-membership-update cofx initial-message admin)]
(testing "the message is a more recent version" (testing "it noops"
(testing "it sets the right participants"))) (is (= (get-in actual [:db :chats chat-id])
(testing "a leave from a member is received" (get-in cofx [:db :chats chat-id]))))))
(testing "the user is removed")))))) (testing "a new message comes in"
(let [actual (group-chats/handle-membership-update cofx
{:chat-id chat-id
:membership-updates [{:from member-1
:events [{:type "chat-created"
:clock-value 1
:name "group-name"}
{:type "admins-added"
:clock-value 10
:members [member-2]}
{:type "admin-removed"
:clock-value 11
:member member-1}]}
{:from member-2
:events [{:type "member-removed"
:clock-value 12
:member member-3}
{:type "members-added"
:clock-value 12
:members [member-4]}]}]}
member-3)
actual-chat (get-in actual [:db :chats chat-id])]
(testing "the chat is updated"
(is actual-chat))
(testing "admins are updated"
(is (= #{member-2} (:admins actual-chat))))
(testing "members are updated"
(is (= #{member-1 member-2 member-4} (:contacts actual-chat))))))))))
(deftest build-group-test
(testing "only adds"
(let [events [{:type "chat-created"
:clock-value 0
:name "chat-name"
:from "1"}
{:type "members-added"
:clock-value 1
:from "1"
:members ["2"]}
{:type "admins-added"
:clock-value 2
:from "1"
:members ["2"]}
{:type "members-added"
:clock-value 3
:from "2"
:members ["3"]}]
expected {:name "chat-name"
:admins #{"1" "2"}
:contacts #{"1" "2" "3"}}]
(is (= expected (group-chats/build-group events)))))
(testing "adds and removes"
(let [events [{:type "chat-created"
:clock-value 0
:name "chat-name"
:from "1"}
{:type "members-added"
:clock-value 1
:from "1"
:members ["2"]}
{:type "admins-added"
:clock-value 2
:from "1"
:members ["2"]}
{:type "admin-removed"
:clock-value 3
:from "2"
:member "2"}
{:type "member-removed"
:clock-value 4
:from "2"
:member "2"}]
expected {:name "chat-name"
:admins #{"1"}
:contacts #{"1"}}]
(is (= expected (group-chats/build-group events)))))
(testing "name changed"
(let [events [{:type "chat-created"
:clock-value 0
:name "chat-name"
:from "1"}
{:type "members-added"
:clock-value 1
:from "1"
:members ["2"]}
{:type "admins-added"
:clock-value 2
:from "1"
:members ["2"]}
{:type "name-changed"
:clock-value 3
:from "2"
:name "new-name"}]
expected {:name "new-name"
:admins #{"1" "2"}
:contacts #{"1" "2"}}]
(is (= expected (group-chats/build-group events)))))
(testing "invalid events"
(let [events [{:type "chat-created"
:name "chat-name"
:clock-value 0
:from "1"}
{:type "admins-added" ; can't make an admin a user not in the group
:clock-value 1
:from "1"
:members ["non-existing"]}
{:type "members-added"
:clock-value 2
:from "1"
:members ["2"]}
{:type "admins-added"
:clock-value 3
:from "1"
:members ["2"]}
{:type "members-added"
:clock-value 4
:from "2"
:members ["3"]}
{:type "admin-removed" ; can't remove an admin from admins unless it's the same user
:clock-value 5
:from "1"
:member "2"}
{:type "member-removed" ; can't remove an admin from the group
:clock-value 6
:from "1"
:member "2"}]
expected {:name "chat-name"
:admins #{"1" "2"}
:contacts #{"1" "2" "3"}}]
(is (= expected (group-chats/build-group events)))))
(testing "out of order-events"
(let [events [{:type "chat-created"
:name "chat-name"
:clock-value 0
:from "1"}
{:type "admins-added"
:clock-value 2
:from "1"
:members ["2"]}
{:type "members-added"
:clock-value 1
:from "1"
:members ["2"]}
{:type "members-added"
:clock-value 3
:from "2"
:members ["3"]}]
expected {:name "chat-name"
:admins #{"1" "2"}
:contacts #{"1" "2" "3"}}]
(is (= expected (group-chats/build-group events))))))
(deftest valid-event-test
(let [multi-admin-group {:admins #{"1" "2"}
:contacts #{"1" "2" "3"}}
single-admin-group {:admins #{"1"}
:contacts #{"1" "2" "3"}}]
(testing "members-added"
(testing "admins can add members"
(is (group-chats/valid-event? multi-admin-group
{:type "members-added" :clock-value 6 :from "1" :members ["4"]})))
(testing "non-admin members cannot add members"
(is (not (group-chats/valid-event? multi-admin-group
{:type "members-added" :clock-value 6 :from "3" :members ["4"]})))))
(testing "admins-added"
(testing "admins can make other member admins"
(is (group-chats/valid-event? multi-admin-group
{:type "admins-added" :clock-value 6 :from "1" :members ["3"]})))
(testing "non-admins can't make other member admins"
(is (not (group-chats/valid-event? multi-admin-group
{:type "admins-added" :clock-value 6 :from "3" :members ["3"]}))))
(testing "non-existing users can't be made admin"
(is (not (group-chats/valid-event? multi-admin-group
{:type "admins-added" :clock-value 6 :from "1" :members ["not-existing"]})))))
(testing "member-removed"
(testing "admins can remove non-admin members"
(is (group-chats/valid-event? multi-admin-group
{:type "member-removed" :clock-value 6 :from "1" :member "3"})))
(testing "admins can't remove themselves"
(is (not (group-chats/valid-event? multi-admin-group
{:type "member-removed" :clock-value 6 :from "1" :member "1"}))))
(testing "participants non-admin can remove themselves"
(is (group-chats/valid-event? multi-admin-group
{:type "member-removed" :clock-value 6 :from "3" :member "3"})))
(testing "non-admin can't remove other members"
(is (not (group-chats/valid-event? multi-admin-group
{:type "member-removed" :clock-value 6 :from "3" :member "1"})))))
(testing "admin-removed"
(testing "admins can remove themselves"
(is (group-chats/valid-event? multi-admin-group
{:type "admin-removed" :clock-value 6 :from "1" :member "1"})))
(testing "admins can't remove other admins"
(is (not (group-chats/valid-event? multi-admin-group
{:type "admin-removed" :clock-value 6 :from "1" :member "2"}))))
(testing "participants non-admin can't remove other admins"
(is (not (group-chats/valid-event? multi-admin-group
{:type "admin-removed" :clock-value 6 :from "3" :member "1"}))))
(testing "the last admin can't be removed"
(is (not (group-chats/valid-event? single-admin-group
{:type "admin-removed" :clock-value 6 :from "1" :member "1"}))))
(testing "name-changed"
(testing "a change from an admin"
(is (group-chats/valid-event? multi-admin-group
{:type "name-changed" :clock-value 6 :from "1" :name "new-name"}))))
(testing "a change from an non-admin"
(is (not (group-chats/valid-event? multi-admin-group
{:type "name-changed" :clock-value 6 :from "3" :name "new-name"}))))
(testing "an empty name"
(is (not (group-chats/valid-event? multi-admin-group
{:type "name-changed" :clock-value 6 :from "1" :name " "})))))))
(deftest create-test
(testing "create a new chat"
(with-redefs [utils.clocks/send inc]
(let [cofx {:random-guid-generator (constantly "random")
:db {:current-public-key "me"
:group/selected-contacts #{"1" "2"}}}]
(is (= {:chat-id "randomme"
:from "me"
:events [{:type "chat-created"
:clock-value 1
:name "group-name"}
{:type "members-added"
:clock-value 2
:members #{"1" "2"}}]}
(:group-chats/sign-membership (group-chats/create cofx "group-name"))))))))
(deftest signature-pairs-test
(let [event-1 {:from "1"
:signature "signature-1"
:events [{:type "a" :name "a" :clock-value 1}
{:type "b" :name "b" :clock-value 2}]}
event-2 {:from "2"
:signature "signature-2"
:events [{:type "c" :name "c" :clock-value 1}
{:type "d" :name "d" :clock-value 2}]}
message {:chat-id "randomme"
:membership-updates [event-1
event-2]}
expected (js/JSON.stringify
(clj->js [[(group-chats/signature-material "randomme" (:events event-1))
"signature-1"]
[(group-chats/signature-material "randomme" (:events event-2))
"signature-2"]]))]
(is (= expected (group-chats/signature-pairs message)))))
(deftest signature-material-test
(is (= (js/JSON.stringify (clj->js [[[["a" "a-value"]
["b" "b-value"]
["c" "c-value"]]
[["a" "a-value"]
["e" "e-value"]]] "chat-id"]))
(group-chats/signature-material "chat-id" [{:b "b-value"
:a "a-value"
:c "c-value"}
{:e "e-value"
:a "a-value"}]))))
(deftest remove-group-chat-test
(with-redefs [utils.clocks/send inc]
(let [cofx {:db {:chats {chat-id {:admins #{admin}
:name "chat-name"
:chat-id chat-id
:is-active true
:group-chat true
:contacts #{member-1 member-2 member-3}
:membership-updates (:membership-updates initial-message)}}}}]
(testing "removing a member"
(is (= {:from member-3
:chat-id chat-id
:events [{:type "member-removed" :member member-3 :clock-value 4}]}
(:group-chats/sign-membership
(group-chats/remove
(assoc-in cofx [:db :current-public-key] member-3)
chat-id)))))
(testing "removing an admin"
(is (not (:group-chats/sign-membership
(group-chats/remove
(assoc-in cofx [:db :current-public-key] member-1)
chat-id))))))))
(deftest add-members-test
(with-redefs [utils.clocks/send inc]
(testing "add-members"
(let [cofx {:db {:current-chat-id chat-id
:selected-participants ["new-member"]
:current-public-key "me"
:chats {chat-id {:membership-updates [{:events [{:clock-value 1}]}]}}}}]
(is (= {:chat-id chat-id
:from "me"
:events [{:type "members-added"
:clock-value 2
:members ["new-member"]}]}
(:group-chats/sign-membership (group-chats/add-members cofx))))))))
(deftest remove-member-test
(with-redefs [utils.clocks/send inc]
(testing "remove-member"
(let [cofx {:db {:current-public-key "me"
:chats {chat-id {:admins #{"me"}
:contacts #{"member"}
:membership-updates [{:events [{:clock-value 1}]}]}}}}]
(is (= {:chat-id chat-id
:from "me"
:events [{:type "member-removed"
:clock-value 2
:member "member"}]}
(:group-chats/sign-membership (group-chats/remove-member cofx chat-id "member"))))))))

View File

@ -2,6 +2,7 @@
(:require [doo.runner :refer-macros [doo-tests]] (:require [doo.runner :refer-macros [doo-tests]]
[status-im.test.contacts.events] [status-im.test.contacts.events]
[status-im.test.contacts.subs] [status-im.test.contacts.subs]
[status-im.test.data-store.chats]
[status-im.test.data-store.realm.core] [status-im.test.data-store.realm.core]
[status-im.test.browser.core] [status-im.test.browser.core]
[status-im.test.browser.permissions] [status-im.test.browser.permissions]
@ -72,6 +73,7 @@
'status-im.test.contacts.events 'status-im.test.contacts.events
'status-im.test.contacts.subs 'status-im.test.contacts.subs
'status-im.test.init.core 'status-im.test.init.core
'status-im.test.data-store.chats
'status-im.test.data-store.realm.core 'status-im.test.data-store.realm.core
'status-im.test.mailserver.core 'status-im.test.mailserver.core
'status-im.test.group-chats.core 'status-im.test.group-chats.core