mirror of
https://github.com/status-im/status-react.git
synced 2025-01-09 10:42:53 +00:00
Move group chats to status-go
This commit completely remove transit for group chats. All the processing is now done in status-go. Also introuduces parsing and handling of mentions, needed so that system messages can be easily built in status-go. Signed-off-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
This commit is contained in:
parent
4a1842285f
commit
54cf783d5b
@ -13,7 +13,6 @@
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[status-im.mailserver.core :as mailserver]
|
||||
[status-im.native-module.core :as status]
|
||||
[status-im.transport.message.group-chat :as message.group-chat]
|
||||
[status-im.transport.message.protocol :as protocol]
|
||||
[status-im.transport.message.transit :as transit]
|
||||
[status-im.transport.utils :as transport.utils]
|
||||
@ -116,12 +115,15 @@
|
||||
(get-in cofx [:db :chats chat-id :public?])) chat-id
|
||||
(and (= constants/message-type-one-to-one message-type)
|
||||
(= (multiaccounts.model/current-public-key cofx) from)) chat-id
|
||||
(= constants/message-type-private-group-system-message message-type) chat-id
|
||||
(= constants/message-type-one-to-one message-type) from))
|
||||
|
||||
(fx/defn update-unviewed-count
|
||||
[{:keys [now db] :as cofx} {:keys [chat-id
|
||||
from
|
||||
message-type
|
||||
message-id] :as message}]
|
||||
(when-not (= message-type constants/message-type-private-group-system-message)
|
||||
(let [{:keys [current-chat-id view-id]} db
|
||||
chat-view? (or (= :chat view-id)
|
||||
(= :chat-modal view-id))
|
||||
@ -138,7 +140,7 @@
|
||||
:else
|
||||
{:db (update-in db [:chats chat-id]
|
||||
assoc
|
||||
:unviewed-messages-count (inc current-count))})))
|
||||
:unviewed-messages-count (inc current-count))}))))
|
||||
|
||||
(fx/defn receive-one
|
||||
[cofx message]
|
||||
|
@ -7,15 +7,15 @@
|
||||
|
||||
(def ms-in-bg-for-require-bioauth 5000)
|
||||
|
||||
(def content-type-text 0)
|
||||
(def content-type-sticker 1)
|
||||
(def content-type-status 2)
|
||||
(def content-type-emoji 3)
|
||||
(def content-type-text 1)
|
||||
(def content-type-sticker 2)
|
||||
(def content-type-status 3)
|
||||
(def content-type-emoji 4)
|
||||
|
||||
(def message-type-one-to-one 0)
|
||||
(def message-type-public-group 1)
|
||||
(def message-type-private-group 2)
|
||||
(def message-type-private-group-system-message 3)
|
||||
(def message-type-one-to-one 1)
|
||||
(def message-type-public-group 2)
|
||||
(def message-type-private-group 3)
|
||||
(def message-type-private-group-system-message 4)
|
||||
|
||||
(def desktop-content-types
|
||||
#{content-type-text content-type-emoji content-type-status})
|
||||
|
@ -34,32 +34,6 @@
|
||||
; Build an event id from a message
|
||||
(def event-id (comp ethereum/sha3 event->string))
|
||||
|
||||
(defn marshal-membership-updates [updates]
|
||||
(mapcat (fn [{:keys [signature events from]}]
|
||||
(map #(-> %
|
||||
(assoc
|
||||
:clockValue (:clock-value %)
|
||||
:id (event-id %)
|
||||
:signature signature
|
||||
:from from)
|
||||
(dissoc :clock-value)) events)) updates))
|
||||
|
||||
(defn unmarshal-membership-updates [chat-id updates]
|
||||
(->> updates
|
||||
(group-by :signature)
|
||||
(map (fn [[signature events]]
|
||||
{:events
|
||||
(into []
|
||||
(sort-by :clock-value (map #(-> %
|
||||
(assoc :clock-value (:clockValue %))
|
||||
(update :members (fn [members] (into #{} members)))
|
||||
(dissoc :signature :from :id :clockValue)
|
||||
remove-empty-vals) events)))
|
||||
:from (-> events first :from)
|
||||
:signature signature
|
||||
:chat-id chat-id}))
|
||||
(into #{})))
|
||||
|
||||
(defn type->rpc [{:keys [public? group-chat] :as chat}]
|
||||
(assoc chat :chatType (cond
|
||||
public? public-chat-type
|
||||
@ -110,10 +84,9 @@
|
||||
(-> chat
|
||||
type->rpc
|
||||
marshal-members
|
||||
(update :membership-updates marshal-membership-updates)
|
||||
(update :last-message messages/->rpc)
|
||||
(clojure.set/rename-keys {:chat-id :id
|
||||
:membership-updates :membershipUpdates
|
||||
:membership-update-events :membershipUpdateEvents
|
||||
:unviewed-messages-count :unviewedMessagesCount
|
||||
:last-message :lastMessage
|
||||
:deleted-at-clock-value :deletedAtClockValue
|
||||
@ -130,13 +103,12 @@
|
||||
rpc->type
|
||||
unmarshal-members
|
||||
(clojure.set/rename-keys {:id :chat-id
|
||||
:membershipUpdates :membership-updates
|
||||
:membershipUpdateEvents :membership-update-events
|
||||
:deletedAtClockValue :deleted-at-clock-value
|
||||
:unviewedMessagesCount :unviewed-messages-count
|
||||
:lastMessage :last-message
|
||||
:active :is-active
|
||||
:lastClockValue :last-clock-value})
|
||||
(update :membership-updates (partial unmarshal-membership-updates (:id chat)))
|
||||
(update :last-message #(when % (messages/<-rpc %)))
|
||||
(dissoc :chatType :members)))
|
||||
|
||||
|
@ -36,6 +36,13 @@
|
||||
"shhext_enableInstallation" {}
|
||||
"shhext_disableInstallation" {}
|
||||
"shhext_sendChatMessage" {}
|
||||
"shhext_confirmJoiningGroup" {}
|
||||
"shhext_addAdminsToGroupChat" {}
|
||||
"shhext_addMembersToGroupChat" {}
|
||||
"shhext_removeMemberFromGroupChat" {}
|
||||
"shhext_leaveGroupChat" {}
|
||||
"shhext_changeGroupChatName" {}
|
||||
"shhext_createGroupChatWithMembers" {}
|
||||
"shhext_reSendChatMessage" {}
|
||||
"shhext_getOurInstallations" {}
|
||||
"shhext_setInstallationMetadata" {}
|
||||
|
@ -1170,17 +1170,6 @@
|
||||
(fn [cofx [_ chat-id]]
|
||||
(group-chats/join-chat cofx chat-id)))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:group-chats.callback/sign-success
|
||||
[(re-frame/inject-cofx :random-guid-generator)]
|
||||
(fn [cofx [_ group-update]]
|
||||
(group-chats/handle-sign-success cofx group-update)))
|
||||
|
||||
(handlers/register-handler-fx
|
||||
:group-chats.callback/extract-signature-success
|
||||
(fn [cofx [_ group-update message-info sender-signature]]
|
||||
(group-chats/handle-membership-update cofx group-update message-info sender-signature)))
|
||||
|
||||
;; profile module
|
||||
|
||||
(handlers/register-handler-fx
|
||||
|
@ -7,6 +7,9 @@
|
||||
[re-frame.core :as re-frame]
|
||||
[status-im.constants :as constants]
|
||||
[status-im.chat.models.message-content :as message-content]
|
||||
[status-im.ui.screens.navigation :as navigation]
|
||||
[status-im.data-store.chats :as data-store.chats]
|
||||
[status-im.data-store.messages :as data-store.messages]
|
||||
[status-im.multiaccounts.core :as multiaccounts]
|
||||
[status-im.multiaccounts.model :as multiaccounts.model]
|
||||
[status-im.utils.pairing :as pairing.utils]
|
||||
@ -17,7 +20,6 @@
|
||||
[status-im.group-chats.db :as group-chats.db]
|
||||
[status-im.i18n :as i18n]
|
||||
[status-im.native-module.core :as native-module]
|
||||
[status-im.transport.message.group-chat :as message.group-chat]
|
||||
[status-im.transport.message.protocol :as protocol]
|
||||
[status-im.utils.clocks :as utils.clocks]
|
||||
[status-im.utils.fx :as fx]
|
||||
@ -25,266 +27,81 @@
|
||||
[status-im.mailserver.topics :as mailserver.topics]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
;; 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 :last-clock-value])))
|
||||
|
||||
(defn- parse-response [response-js]
|
||||
(-> response-js
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true)))
|
||||
|
||||
(defn extract-creator
|
||||
"Takes a chat as an input, returns the creator"
|
||||
[{:keys [membership-updates]}]
|
||||
(->> membership-updates
|
||||
(filter (fn [{:keys [events]}]
|
||||
(some #(= "chat-created" (:type %)) events)))
|
||||
first
|
||||
:from))
|
||||
|
||||
(defn creator? [public-key chat]
|
||||
(= public-key (extract-creator chat)))
|
||||
|
||||
(defn signature-material
|
||||
"Transform an update into a signable string"
|
||||
[chat-id events]
|
||||
(js/JSON.stringify
|
||||
(clj->js [(mapv event->vector (sort-events events)) chat-id])))
|
||||
|
||||
(defn signature-pairs
|
||||
"Transform a bunch of updates into signable pairs to be verified"
|
||||
[{:keys [chat-id membership-updates] :as payload}]
|
||||
(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?
|
||||
;; We need to make sure the chat-id ends with the admin pk (and it's not the same).
|
||||
;; this is due to prevent an attack whereby a non-admin user would
|
||||
;; send out a message with identical chat-id and themselves as admin to other members,
|
||||
;; who would then have to trust the first of the two messages received, possibly
|
||||
;; resulting in a situation where some of the members in the chat trust a different admin.
|
||||
[chat-id admin]
|
||||
(and (string/ends-with? 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)
|
||||
"member-joined" (and (contacts member)
|
||||
(= from member))
|
||||
"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
|
||||
(= from member))
|
||||
"admin-removed" (and (admins from)
|
||||
(= from member))
|
||||
false)))
|
||||
|
||||
(defn send-membership-update
|
||||
"Send a membership update to all participants but the sender"
|
||||
([cofx payload chat-id]
|
||||
(send-membership-update cofx payload chat-id nil))
|
||||
([{:keys [message-id] :as cofx} payload chat-id removed-members]
|
||||
(let [chat (get-in cofx [:db :chats chat-id])
|
||||
creator (extract-creator chat)
|
||||
members (clojure.set/union (get-in cofx [:db :chats chat-id :contacts])
|
||||
removed-members)
|
||||
current-public-key (multiaccounts.model/current-public-key cofx)
|
||||
members-allowed (filter
|
||||
(fn [pk]
|
||||
(if (= pk current-public-key)
|
||||
(pairing.utils/has-paired-installations? cofx)
|
||||
true))
|
||||
members)
|
||||
destinations (map (fn [member]
|
||||
{:public-key member})
|
||||
members-allowed)]
|
||||
(fx/merge
|
||||
cofx
|
||||
{:shh/send-group-message
|
||||
{:src current-public-key
|
||||
:dsts destinations
|
||||
:success-event [:transport/message-sent
|
||||
chat-id
|
||||
(:message cofx)
|
||||
constants/message-type-private-group]
|
||||
:payload payload}}))))
|
||||
|
||||
(fx/defn handle-membership-update-received
|
||||
"Extract signatures in status-go and act if successful"
|
||||
[cofx membership-update signature message-info]
|
||||
{:group-chats/extract-membership-signature [[membership-update message-info signature]]})
|
||||
|
||||
(defn chat->group-update
|
||||
"Transform a chat in a GroupMembershipUpdate"
|
||||
[chat-id {:keys [membership-updates]}]
|
||||
(message.group-chat/map->GroupMembershipUpdate. {:chat-id chat-id
|
||||
:membership-updates membership-updates}))
|
||||
|
||||
(defn handle-sign-response
|
||||
"Callback to dispatch on sign response"
|
||||
[payload response-js]
|
||||
(let [{:keys [error signature]} (parse-response response-js)]
|
||||
(if error
|
||||
(re-frame/dispatch [:group-chats.callback/sign-failed error])
|
||||
(re-frame/dispatch [:group-chats.callback/sign-success (assoc payload :signature signature)]))))
|
||||
|
||||
(defn add-identities
|
||||
"Add verified identities extracted from the signature to the updates"
|
||||
[payload identities]
|
||||
(update payload :membership-updates (fn [updates]
|
||||
(map
|
||||
#(assoc %1 :from (str "0x" %2))
|
||||
updates
|
||||
identities))))
|
||||
|
||||
(defn handle-extract-signature-response
|
||||
"Callback to dispatch on extract signature response"
|
||||
[payload message-info 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) message-info 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)))
|
||||
|
||||
(defn extract-membership-signature [payload message-info sender]
|
||||
(native-module/extract-group-membership-signatures
|
||||
(signature-pairs payload)
|
||||
(partial handle-extract-signature-response payload message-info sender)))
|
||||
|
||||
(defn- members-added-event [last-clock-value members]
|
||||
{:type "members-added"
|
||||
:clock-value (utils.clocks/send last-clock-value)
|
||||
:members members})
|
||||
|
||||
(defn- member-joined-event [last-clock-value member]
|
||||
{:type "member-joined"
|
||||
:clock-value (utils.clocks/send last-clock-value)
|
||||
:member member})
|
||||
|
||||
(fx/defn create
|
||||
"Format group update message and sign membership"
|
||||
[{:keys [db random-guid-generator] :as cofx} group-name]
|
||||
(let [my-public-key (multiaccounts.model/current-public-key cofx)
|
||||
chat-id (str (random-guid-generator) my-public-key)
|
||||
selected-contacts (:group/selected-contacts db)
|
||||
clock-value (utils.clocks/send 0)
|
||||
create-event {:type "chat-created"
|
||||
:name group-name
|
||||
:clock-value clock-value}
|
||||
events [create-event
|
||||
(members-added-event clock-value selected-contacts)]]
|
||||
|
||||
(fx/merge
|
||||
cofx
|
||||
{:group-chats/sign-membership {:chat-id chat-id
|
||||
:from my-public-key
|
||||
:events events}
|
||||
: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 (multiaccounts.model/current-public-key cofx)
|
||||
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]}})))
|
||||
{::json-rpc/call [{:method "shhext_removeMemberFromGroupChat"
|
||||
:params [nil chat-id member]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})
|
||||
|
||||
(fx/defn join-chat
|
||||
"Format group update message and sign membership"
|
||||
[cofx chat-id]
|
||||
(fx/defn set-up-filter
|
||||
"Listen/Tear down the shared topic/contact-codes. Stop listening for members who
|
||||
have left the chat"
|
||||
[cofx chat-id previous-chat]
|
||||
(let [my-public-key (multiaccounts.model/current-public-key cofx)
|
||||
last-clock-value (get-last-clock-value cofx chat-id)
|
||||
chat (get-in cofx [:db :chats chat-id])
|
||||
event (member-joined-event last-clock-value my-public-key)]
|
||||
(when (valid-event? chat (assoc event
|
||||
:from
|
||||
my-public-key))
|
||||
new-chat (get-in cofx [:db :chats chat-id])
|
||||
members (:members-joined new-chat)]
|
||||
;; If we left the chat do nothing
|
||||
(when-not (and (group-chats.db/joined? my-public-key previous-chat)
|
||||
(not (group-chats.db/joined? my-public-key new-chat)))
|
||||
(fx/merge
|
||||
cofx
|
||||
{:group-chats/sign-membership {:chat-id chat-id
|
||||
:from my-public-key
|
||||
:events [event]}}))))
|
||||
(transport.filters/upsert-group-chat-topics)
|
||||
(transport.filters/load-members members)))))
|
||||
|
||||
(fx/defn handle-chat-update
|
||||
{:events [::chat-updated]}
|
||||
[cofx {:keys [chats messages]}]
|
||||
(let [{:keys [chat-id] :as chat} (-> chats
|
||||
first
|
||||
(data-store.chats/<-rpc))
|
||||
|
||||
previous-chat (get-in cofx [:chats chat-id])
|
||||
set-up-filter-fx (set-up-filter chat-id previous-chat)
|
||||
chat-fx (models.chat/ensure-chat
|
||||
(dissoc chat :unviewed-messages-count))
|
||||
messages-fx (map #(models.message/receive-one
|
||||
(data-store.messages/<-rpc %))
|
||||
messages)
|
||||
navigate-fx #(if (get-in % [:db :chats chat-id :is-active])
|
||||
(models.chat/navigate-to-chat % chat-id {:navigation-reset? true})
|
||||
(navigation/navigate-to-cofx % :home {}))]
|
||||
|
||||
(apply fx/merge cofx (concat [chat-fx]
|
||||
messages-fx
|
||||
[navigate-fx]))))
|
||||
|
||||
(fx/defn join-chat
|
||||
[cofx chat-id]
|
||||
{::json-rpc/call [{:method "shhext_confirmJoiningGroup"
|
||||
:params [chat-id]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})
|
||||
|
||||
(fx/defn create
|
||||
[{:keys [db] :as cofx} group-name]
|
||||
(let [selected-contacts (:group/selected-contacts db)]
|
||||
{::json-rpc/call [{:method "shhext_createGroupChatWithMembers"
|
||||
:params [nil group-name (into [] selected-contacts)]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]}))
|
||||
|
||||
(fx/defn make-admin
|
||||
"Format group update with make admin message and sign membership"
|
||||
[{:keys [db] :as cofx} chat-id member]
|
||||
(let [my-public-key (multiaccounts.model/current-public-key cofx)
|
||||
last-clock-value (get-last-clock-value cofx chat-id)
|
||||
chat (get-in cofx [:db :chats chat-id])
|
||||
event {:type "admins-added"
|
||||
:members [member]
|
||||
:clock-value (utils.clocks/send last-clock-value)}]
|
||||
(when (valid-event? chat (assoc event
|
||||
:from
|
||||
my-public-key))
|
||||
{:group-chats/sign-membership {:chat-id chat-id
|
||||
:from my-public-key
|
||||
:events [event]}})))
|
||||
{::json-rpc/call [{:method "shhext_addAdminsToGroupChat"
|
||||
:params [nil chat-id [member]]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})
|
||||
|
||||
(fx/defn add-members
|
||||
"Add members to a group chat"
|
||||
[{{:keys [current-chat-id selected-participants]} :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 (multiaccounts.model/current-public-key cofx)
|
||||
:events events}}))
|
||||
{::json-rpc/call [{:method "shhext_addMembersToGroupChat"
|
||||
:params [nil current-chat-id selected-participants]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})
|
||||
(fx/defn remove
|
||||
"Remove & leave chat"
|
||||
[{:keys [db] :as cofx} chat-id]
|
||||
(let [my-public-key (multiaccounts.model/current-public-key cofx)]
|
||||
(fx/merge cofx
|
||||
(remove-member chat-id my-public-key)
|
||||
(models.chat/remove-chat chat-id))))
|
||||
{::json-rpc/call [{:method "shhext_leaveGroupChat"
|
||||
:params [nil chat-id]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})
|
||||
|
||||
(defn- valid-name? [name]
|
||||
(spec/valid? :profile/name name))
|
||||
@ -302,226 +119,10 @@
|
||||
(fx/defn save
|
||||
"Save chat from edited profile"
|
||||
[{:keys [db] :as cofx}]
|
||||
(let [current-chat-id (get-in cofx [:db :current-chat-id])
|
||||
my-public-key (get-in db [:multiaccount :public-key])
|
||||
last-clock-value (get-last-clock-value cofx current-chat-id)
|
||||
new-name (get-in cofx [:db :group-chat-profile/profile :name])
|
||||
name-changed-event {:type "name-changed"
|
||||
:name new-name
|
||||
:clock-value (utils.clocks/send last-clock-value)}]
|
||||
(let [new-name (get-in cofx [:db :group-chat-profile/profile :name])
|
||||
current-chat-id (:current-chat-id db)]
|
||||
(when (valid-name? new-name)
|
||||
(fx/merge cofx
|
||||
{:db (assoc db
|
||||
:group-chat-profile/editing?
|
||||
false)
|
||||
:group-chats/sign-membership {:chat-id current-chat-id
|
||||
:from my-public-key
|
||||
:events [name-changed-event]}}))))
|
||||
{::json-rpc/call [{:method "shhext_changeGroupChatName"
|
||||
:params [nil current-chat-id new-name]
|
||||
:on-success #(re-frame/dispatch [::chat-updated %])}]})))
|
||||
|
||||
(defn process-event
|
||||
"Add/remove an event to a group, carrying the clock-value at which it happened"
|
||||
[group {:keys [type member members chat-id clock-value from name] :as event}]
|
||||
(if (valid-event? group event)
|
||||
(case type
|
||||
"chat-created" {:name name
|
||||
:created-at clock-value
|
||||
:admins #{from}
|
||||
:members-joined #{from}
|
||||
:contacts #{from}}
|
||||
"name-changed" (assoc group
|
||||
:name name
|
||||
:name-changed-by from
|
||||
:name-changed-at clock-value)
|
||||
"members-added" (as-> group $
|
||||
(update $ :contacts clojure.set/union (set members))
|
||||
(reduce (fn [acc member] (assoc-in acc [member :added] clock-value)) $ members))
|
||||
"member-joined" (-> group
|
||||
(update :members-joined conj member)
|
||||
(assoc-in [member :joined] clock-value))
|
||||
"admins-added" (as-> group $
|
||||
(update $ :admins clojure.set/union (set members))
|
||||
(reduce (fn [acc member] (assoc-in acc [member :admin-added] clock-value)) $ members))
|
||||
"member-removed" (-> group
|
||||
(update :contacts disj member)
|
||||
(update :admins disj member)
|
||||
(update :members-joined disj member)
|
||||
(assoc-in [member :removed] clock-value))
|
||||
"admin-removed" (-> group
|
||||
(update :admins disj member)
|
||||
(assoc-in [member :admin-removed] clock-value)))
|
||||
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 #{}
|
||||
:members-joined #{}
|
||||
:contacts #{}})))
|
||||
|
||||
(defn membership-changes->system-messages [cofx
|
||||
clock-values
|
||||
{:keys [chat-id
|
||||
chat-name
|
||||
creator
|
||||
members-added
|
||||
members-joined
|
||||
admins-added
|
||||
name-changed?
|
||||
members-removed]}]
|
||||
(let [get-contact (partial models.contact/build-contact cofx)
|
||||
format-message (fn [contact text clock-value]
|
||||
{:chat-id chat-id
|
||||
:text text
|
||||
:clock-value clock-value
|
||||
:from (:public-key contact)})
|
||||
creator-contact (when creator (get-contact creator))
|
||||
name-changed-author (when name-changed? (get-contact (:name-changed-by clock-values)))
|
||||
admins-added (map
|
||||
get-contact
|
||||
(disj admins-added creator))
|
||||
contacts-added (map
|
||||
get-contact
|
||||
(disj members-added creator))
|
||||
contacts-joined (map
|
||||
get-contact
|
||||
(disj members-joined creator))
|
||||
contacts-removed (map
|
||||
get-contact
|
||||
members-removed)]
|
||||
(cond-> []
|
||||
creator-contact (conj (format-message creator-contact
|
||||
(i18n/label :t/group-chat-created
|
||||
{:name chat-name
|
||||
:member (multiaccounts/displayed-name creator-contact)})
|
||||
(:created-at clock-values)))
|
||||
name-changed? (conj (format-message name-changed-author
|
||||
(i18n/label :t/group-chat-name-changed
|
||||
{:name chat-name
|
||||
:member (multiaccounts/displayed-name name-changed-author)})
|
||||
(:name-changed-at clock-values)))
|
||||
(seq members-added) (concat (map #(format-message
|
||||
%
|
||||
(i18n/label :t/group-chat-member-added {:member (multiaccounts/displayed-name %)})
|
||||
(get-in clock-values [(:public-key %) :added]))
|
||||
contacts-added))
|
||||
(seq members-joined) (concat (map #(format-message
|
||||
%
|
||||
(i18n/label :t/group-chat-member-joined {:member (multiaccounts/displayed-name %)})
|
||||
(get-in clock-values [(:public-key %) :joined]))
|
||||
contacts-joined))
|
||||
(seq admins-added) (concat (map #(format-message
|
||||
%
|
||||
(i18n/label :t/group-chat-admin-added {:member (multiaccounts/displayed-name %)})
|
||||
(get-in clock-values [(:public-key %) :admin-added]))
|
||||
admins-added))
|
||||
(seq members-removed) (concat (map #(format-message
|
||||
%
|
||||
(i18n/label :t/group-chat-member-removed {:member (multiaccounts/displayed-name %)})
|
||||
(get-in clock-values [(:public-key %) :removed]))
|
||||
contacts-removed)))))
|
||||
|
||||
(fx/defn add-system-messages [cofx chat-id previous-chat clock-values]
|
||||
(let [current-chat (get-in cofx [:db :chats chat-id])
|
||||
current-public-key (get-in cofx [:db :current-public-key])
|
||||
name-changed? (and (seq previous-chat)
|
||||
(not= (:name previous-chat) (:name current-chat)))
|
||||
members-added (clojure.set/difference (:contacts current-chat) (:contacts previous-chat))
|
||||
members-joined (clojure.set/difference (:members-joined current-chat) (:members-joined previous-chat))
|
||||
members-removed (clojure.set/difference (:contacts previous-chat) (:contacts current-chat))
|
||||
admins-added (clojure.set/difference (:admins current-chat) (:admins previous-chat))
|
||||
membership-changes (cond-> {:chat-id chat-id
|
||||
:name-changed? name-changed?
|
||||
:chat-name (:name current-chat)
|
||||
:admins-added admins-added
|
||||
:members-added members-added
|
||||
:members-joined members-joined
|
||||
:members-removed members-removed}
|
||||
(nil? previous-chat)
|
||||
(assoc :creator (extract-creator current-chat)))]
|
||||
(when (or name-changed?
|
||||
(seq admins-added)
|
||||
(seq members-added)
|
||||
(seq members-joined)
|
||||
(seq members-removed))
|
||||
(->> membership-changes
|
||||
(membership-changes->system-messages cofx clock-values)
|
||||
(models.message/add-system-messages cofx)))))
|
||||
|
||||
(fx/defn set-up-filter
|
||||
"Listen/Tear down the shared topic/contact-codes. Stop listening for members who
|
||||
have left the chat"
|
||||
[cofx chat-id previous-chat]
|
||||
(let [my-public-key (multiaccounts.model/current-public-key cofx)
|
||||
new-chat (get-in cofx [:db :chats chat-id])
|
||||
members (:members-joined new-chat)]
|
||||
;; If we left the chat do nothing
|
||||
(when-not (and (group-chats.db/joined? my-public-key previous-chat)
|
||||
(not (group-chats.db/joined? my-public-key new-chat)))
|
||||
(fx/merge
|
||||
cofx
|
||||
(transport.filters/upsert-group-chat-topics)
|
||||
(transport.filters/load-members members)))))
|
||||
|
||||
(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}
|
||||
{:keys [whisper-timestamp metadata]}
|
||||
sender-signature]
|
||||
(let [dev-mode? (get-in cofx [:db :multiaccount :dev-mode?])]
|
||||
(when (valid-chat-id? chat-id (extract-creator membership-update))
|
||||
(let [previous-chat (get-in cofx [:db :chats chat-id])
|
||||
all-updates (clojure.set/union (set (:membership-updates previous-chat))
|
||||
(set (:membership-updates membership-update)))
|
||||
my-public-key (multiaccounts.model/current-public-key cofx)
|
||||
unwrapped-events (group-chats.db/unwrap-events all-updates)
|
||||
new-group (build-group unwrapped-events)
|
||||
member? (contains? (:contacts new-group) my-public-key)]
|
||||
(fx/merge cofx
|
||||
(models.chat/upsert-chat {:chat-id chat-id
|
||||
:name (:name new-group)
|
||||
:is-active (or member?
|
||||
(get previous-chat :is-active true))
|
||||
:group-chat true
|
||||
:membership-updates (into [] all-updates)
|
||||
:admins (:admins new-group)
|
||||
:members-joined (:members-joined new-group)
|
||||
:contacts (:contacts new-group)})
|
||||
(add-system-messages chat-id previous-chat new-group)
|
||||
|
||||
(set-up-filter chat-id previous-chat))))))
|
||||
|
||||
(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 (multiaccounts.model/current-public-key cofx)
|
||||
group-update (chat->group-update chat-id updated-chat)
|
||||
new-group-fx (handle-membership-update group-update nil 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
|
||||
:group-chats/sign-membership
|
||||
sign-membership)
|
||||
|
||||
(re-frame/reg-fx
|
||||
:group-chats/extract-membership-signature
|
||||
(fn [signatures]
|
||||
(doseq [[payload raw-payload sender] signatures]
|
||||
(extract-membership-signature payload raw-payload sender))))
|
||||
|
@ -6,18 +6,11 @@
|
||||
[status-im.ens.core :as ens]
|
||||
[status-im.pairing.core :as pairing]
|
||||
[status-im.transport.message.contact :as transport.contact]
|
||||
[status-im.transport.message.group-chat :as transport.group-chat]
|
||||
[status-im.transport.message.pairing :as transport.pairing]
|
||||
[status-im.transport.message.core :as transport.message]
|
||||
|
||||
[status-im.transport.message.protocol :as protocol]))
|
||||
|
||||
(extend-type transport.group-chat/GroupMembershipUpdate
|
||||
protocol/StatusMessage
|
||||
(receive [this _ signature timestamp {:keys [metadata js-obj] :as cofx}]
|
||||
(group-chats/handle-membership-update-received cofx this signature {:whisper-timestamp timestamp
|
||||
:metadata metadata})))
|
||||
|
||||
(extend-type transport.contact/ContactRequest
|
||||
protocol/StatusMessage
|
||||
(receive [this _ signature timestamp cofx]
|
||||
|
@ -6,14 +6,8 @@
|
||||
[status-im.transport.db :as transport.db]
|
||||
[status-im.transport.message.pairing :as transport.pairing]
|
||||
[status-im.transport.message.contact :as transport.contact]
|
||||
[status-im.transport.message.group-chat :as transport.group-chat]
|
||||
[status-im.transport.message.protocol :as protocol]))
|
||||
|
||||
(extend-type transport.group-chat/GroupMembershipUpdate
|
||||
protocol/StatusMessage
|
||||
(send [this chat-id cofx]
|
||||
(group-chats/send-membership-update cofx this chat-id)))
|
||||
|
||||
(extend-type transport.pairing/PairInstallation
|
||||
protocol/StatusMessage
|
||||
(send [this _ cofx]
|
||||
|
@ -1,12 +0,0 @@
|
||||
(ns status-im.transport.message.group-chat
|
||||
(:require [cljs.spec.alpha :as spec]
|
||||
[status-im.transport.message.protocol :as protocol]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defrecord GroupMembershipUpdate
|
||||
[chat-id membership-updates message]
|
||||
protocol/StatusMessage
|
||||
(validate [this]
|
||||
(if (spec/valid? :message/group-membership-update this)
|
||||
this
|
||||
(log/warn "failed group membership validation" (spec/explain :message/group-membership-update this)))))
|
@ -2,7 +2,6 @@
|
||||
status-im.transport.message.transit
|
||||
(:require [status-im.transport.message.contact :as contact]
|
||||
[status-im.transport.message.protocol :as protocol]
|
||||
[status-im.transport.message.group-chat :as group-chat]
|
||||
[status-im.transport.message.pairing :as pairing]
|
||||
[status-im.constants :as constants]
|
||||
[cognitect.transit :as transit]))
|
||||
@ -50,12 +49,6 @@
|
||||
;; no need for legacy conversions for rest of the content types
|
||||
#js [content content-type message-type clock-value timestamp])))
|
||||
|
||||
(deftype GroupMembershipUpdateHandler []
|
||||
Object
|
||||
(tag [this v] "g5")
|
||||
(rep [this {:keys [chat-id membership-updates message]}]
|
||||
#js [chat-id membership-updates message]))
|
||||
|
||||
(deftype SyncInstallationHandler []
|
||||
Object
|
||||
(tag [this v] "p1")
|
||||
@ -74,7 +67,6 @@
|
||||
contact/ContactRequestConfirmed (ContactRequestConfirmedHandler.)
|
||||
contact/ContactUpdate (ContactUpdateHandler.)
|
||||
protocol/Message (MessageHandler.)
|
||||
group-chat/GroupMembershipUpdate (GroupMembershipUpdateHandler.)
|
||||
pairing/SyncInstallation (SyncInstallationHandler.)
|
||||
pairing/PairInstallation (PairInstallationHandler.)}}))
|
||||
|
||||
@ -109,8 +101,6 @@
|
||||
"c5" (fn [])
|
||||
"c6" (fn [[name profile-image address _ _]]
|
||||
(contact/ContactUpdate. name profile-image address nil nil))
|
||||
"g5" (fn [[chat-id membership-updates message]]
|
||||
(group-chat/GroupMembershipUpdate. chat-id membership-updates message))
|
||||
"p1" (fn [[contacts account chat]]
|
||||
(pairing/SyncInstallation. contacts account chat))
|
||||
"p2" (fn [[installation-id device-type name _]]
|
||||
|
@ -67,6 +67,8 @@
|
||||
purple
|
||||
"#8B3131"])
|
||||
|
||||
(def mention-incoming "#0DA4C9")
|
||||
(def mention-outgoing "#9EE8FA")
|
||||
(def text black)
|
||||
(def text-gray gray)
|
||||
|
||||
|
@ -18,6 +18,10 @@
|
||||
[status-im.utils.platform :as platform])
|
||||
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
|
||||
|
||||
(defview mention-element [from]
|
||||
(letsubs [{:keys [ens-name alias]} [:contacts/contact-name-by-identity from]]
|
||||
(str "@" (or ens-name alias))))
|
||||
|
||||
(defn message-timestamp
|
||||
[t justify-timestamp? outgoing content content-type]
|
||||
[react/text {:style (style/message-timestamp-text
|
||||
@ -86,6 +90,14 @@
|
||||
[:browser.ui/message-link-pressed destination])))}
|
||||
destination])
|
||||
|
||||
"mention"
|
||||
(conj acc [react/text-class
|
||||
{:style {:color (if outgoing colors/mention-outgoing colors/mention-incoming)}
|
||||
:on-press
|
||||
#(re-frame/dispatch
|
||||
[:chat.ui/start-chat literal {:navigation-reset? true}])}
|
||||
[mention-element literal]])
|
||||
|
||||
"status-tag"
|
||||
(conj acc [react/text-class
|
||||
{:style {:color (if outgoing colors/white colors/blue)
|
||||
|
@ -2,7 +2,7 @@
|
||||
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
|
||||
"owner": "status-im",
|
||||
"repo": "status-go",
|
||||
"version": "v0.36.2",
|
||||
"commit-sha1": "4c0d8dedea10b02bbad476170cc0eef61a92ecbf",
|
||||
"src-sha256": "0a13rk9p13s3p1dz3n7wbb3s343dlqsidmphxz57xw6di2s40nzx"
|
||||
"version": "v0.38.0",
|
||||
"commit-sha1": "baa0767c263fa25fdd8d90a42d13f1f1fffce804",
|
||||
"src-sha256": "0m7baga353vyjqb8zlzd22w68kkm24baq8059a89xdv57ln35axi"
|
||||
}
|
||||
|
@ -861,14 +861,13 @@ class TestProfileMultipleDevice(MultipleDeviceTestCase):
|
||||
self.errors.append('Public chat "%s" doesn\'t appear on other device when devices are paired'
|
||||
% public_chat_before_sync_name)
|
||||
|
||||
# Disabling for now until GroupMembershipUpdate is also moved to status-go
|
||||
#device_2_home.element_by_text(group_chat_name).click()
|
||||
#device_2_group_chat = device_2_home.get_chat_view()
|
||||
device_2_home.element_by_text(group_chat_name).click()
|
||||
device_2_group_chat = device_2_home.get_chat_view()
|
||||
|
||||
#if not device_2_group_chat.chat_element_by_text(message_after_sync).is_element_displayed():
|
||||
# self.errors.append('"%s" message in group chat is not synced' % message_after_sync)
|
||||
if not device_2_group_chat.chat_element_by_text(message_after_sync).is_element_displayed():
|
||||
self.errors.append('"%s" message in group chat is not synced' % message_after_sync)
|
||||
|
||||
#device_2_group_chat.get_back_to_home_view()
|
||||
device_2_group_chat.get_back_to_home_view()
|
||||
device_2_home.profile_button.click()
|
||||
if not device_2_profile.profile_picture.is_element_image_equals_template('sauce_logo_red_profile.png'):
|
||||
self.errors.append('Profile picture was not updated after changing when devices are paired')
|
||||
|
@ -5,19 +5,19 @@ from views.sign_in_view import SignInView
|
||||
|
||||
|
||||
def return_left_chat_system_message(username):
|
||||
return "%s left the group" % username
|
||||
return "@%s left the group" % username
|
||||
|
||||
|
||||
def return_created_chat_system_message(username, chat_name):
|
||||
return "%s created the group %s" % (username, chat_name)
|
||||
return "@%s created the group %s" % (username, chat_name)
|
||||
|
||||
|
||||
def return_joined_chat_system_message(username):
|
||||
return "%s has joined the group" % username
|
||||
return "@%s joined the group" % username
|
||||
|
||||
|
||||
def return_made_admin_system_message(username):
|
||||
return "%s has been made admin" % username
|
||||
return "@%s has been made admin" % username
|
||||
|
||||
|
||||
def create_users(driver_1, driver_2):
|
||||
|
@ -13,15 +13,7 @@
|
||||
:admins #{"a" "b"}
|
||||
:members-joined #{"a" "c"}
|
||||
:name "name"
|
||||
:membership-updates [{:chat-id "chat-id"
|
||||
:from "a"
|
||||
:signature "b"
|
||||
:events [{:type "chat-created"
|
||||
:name "test"
|
||||
:clock-value 1}
|
||||
{:type "members-added"
|
||||
:clock-value 2
|
||||
:members ["a" "b"]}]}]
|
||||
:membership-update-events :events
|
||||
:gaps-loaded? true
|
||||
:unviewed-messages-count 2
|
||||
:is-active true
|
||||
@ -49,25 +41,13 @@
|
||||
:admin false
|
||||
:joined false}}
|
||||
:lastClockValue 10
|
||||
:membershipUpdates #{{:type "chat-created"
|
||||
:name "test"
|
||||
:clockValue 1
|
||||
:id "0xcdf4a63e0c98d0018cf532b3a48350bb80e292cc46249e3f876aaa65eb97a231"
|
||||
:signature "b"
|
||||
:from "a"}
|
||||
{:type "members-added"
|
||||
:clockValue 2
|
||||
:members ["a" "b"]
|
||||
:id "0x1c34d6b4d022c432b7eb6b645e095791af0c6bdb626db7d705e6db0d7cd74b56"
|
||||
:signature "b"
|
||||
:from "a"}}
|
||||
:membershipUpdateEvents :events
|
||||
:unviewedMessagesCount 2
|
||||
:active true
|
||||
:timestamp 2}]
|
||||
(testing "marshaling chat"
|
||||
(is (= expected-chat (-> (chats/->rpc chat)
|
||||
(update :members #(into #{} %))
|
||||
(update :membershipUpdates #(into #{} %))))))))
|
||||
(update :members #(into #{} %))))))))
|
||||
|
||||
(deftest normalize-chat-test
|
||||
(let [chat {:id "chat-id"
|
||||
@ -87,18 +67,7 @@
|
||||
:admin false
|
||||
:joined false}]
|
||||
:lastClockValue 10
|
||||
:membershipUpdates [{:type "chat-created"
|
||||
:name "test"
|
||||
:clockValue 1
|
||||
:id "0xcdf4a63e0c98d0018cf532b3a48350bb80e292cc46249e3f876aaa65eb97a231"
|
||||
:signature "b"
|
||||
:from "a"}
|
||||
{:type "members-added"
|
||||
:clockValue 2
|
||||
:members ["a" "b"]
|
||||
:id "0x1c34d6b4d022c432b7eb6b645e095791af0c6bdb626db7d705e6db0d7cd74b56"
|
||||
:signature "b"
|
||||
:from "a"}]
|
||||
:membershipUpdateEvents :events
|
||||
:unviewedMessagesCount 2
|
||||
:active true
|
||||
:timestamp 2}
|
||||
@ -111,34 +80,10 @@
|
||||
:admins #{"a" "b"}
|
||||
:members-joined #{"a" "c"}
|
||||
:name "name"
|
||||
:membership-updates #{{:chat-id "chat-id"
|
||||
:from "a"
|
||||
:signature "b"
|
||||
:events [{:type "chat-created"
|
||||
:name "test"
|
||||
:clock-value 1}
|
||||
{:type "members-added"
|
||||
:clock-value 2
|
||||
:members #{"a" "b"}}]}}
|
||||
:membership-update-events :events
|
||||
:unviewed-messages-count 2
|
||||
:is-active true
|
||||
:chat-id "chat-id"
|
||||
:timestamp 2}]
|
||||
(testing "from-rpc"
|
||||
(is (= expected-chat (chats/<-rpc chat))))))
|
||||
|
||||
(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" :clockValue 10 :from "id-2" :members [1 2] :signature "a" :id "0xb7690375de21da4890d2d5acca8b56e327d9eb75fd3b4bcceca4bf1679c2f830"}
|
||||
{:type "member-removed" :clockValue 11 :from "id-2" :member 1 :signature "a" :id "0x2a66f195abf6e6903c4245e372e1e2e6aea2b2c0a74ad03080a313e94197a64f"}
|
||||
{:type "chat-created" :clockValue 0 :from "id-1" :name "blah" :signature "b" :id "0x7fad22accf1dec64daedf83e7af19b0dcde8c5facfb479874a48da5fb6967e07"}}
|
||||
actual (into #{} (chats/marshal-membership-updates raw-updates))]
|
||||
(is (= expected actual))))
|
||||
|
@ -1,553 +0,0 @@
|
||||
(ns status-im.test.group-chats.core
|
||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||
[status-im.utils.gfycat.core :as gfycat]
|
||||
[status-im.utils.identicon :as identicon]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.utils.clocks :as utils.clocks]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.group-chats.core :as group-chats]))
|
||||
|
||||
(def random-id "685a9351-417e-587c-8bc1-191ac2a57ef8")
|
||||
(def chat-name "chat-name")
|
||||
|
||||
(def member-1 "member-1")
|
||||
(def member-2 "member-2")
|
||||
(def member-3 "member-3")
|
||||
(def member-4 "member-4")
|
||||
|
||||
(def admin member-1)
|
||||
|
||||
(def chat-id (str random-id admin))
|
||||
|
||||
(def initial-message {:chat-id chat-id
|
||||
:membership-updates [{:from admin
|
||||
:events [{:type "chat-created"
|
||||
:name "chat-name"
|
||||
:clock-value 1}
|
||||
{:type "members-added"
|
||||
:clock-value 3
|
||||
:members [member-2 member-3]}]}]})
|
||||
|
||||
(deftest get-last-clock-value-test
|
||||
(is (= 3 (group-chats/get-last-clock-value {:db {:chats {chat-id {:last-clock-value 3}}}} chat-id))))
|
||||
|
||||
(deftest handle-group-membership-update-test
|
||||
(with-redefs [gfycat/generate-gfy (constantly "generated")
|
||||
identicon/identicon (constantly "generated")]
|
||||
|
||||
(testing "a brand new chat"
|
||||
(let [actual (->
|
||||
(group-chats/handle-membership-update {:now 0 :db {}} initial-message "payload" admin)
|
||||
:db
|
||||
:chats
|
||||
(get chat-id))]
|
||||
(testing "it creates a new chat"
|
||||
(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"
|
||||
(is (= #{member-1 member-2 member-3}
|
||||
(:contacts actual))))
|
||||
(testing "it sets the updates"
|
||||
(is (= (:membership-updates initial-message)
|
||||
(:membership-updates actual))))
|
||||
(testing "it sets the right admins"
|
||||
(is (= #{admin}
|
||||
(:admins actual))))))
|
||||
(testing "a chat with the wrong id"
|
||||
(let [bad-chat-id (str random-id member-2)
|
||||
actual (->
|
||||
(group-chats/handle-membership-update
|
||||
{:now 0 :db {}}
|
||||
(assoc initial-message :chat-id bad-chat-id)
|
||||
"payload"
|
||||
admin)
|
||||
:db
|
||||
:chats
|
||||
(get bad-chat-id))]
|
||||
(testing "it does not create a chat"
|
||||
(is (not actual)))))
|
||||
(testing "an already existing chat"
|
||||
(let [cofx (assoc
|
||||
(group-chats/handle-membership-update {:now 0 :db {:multiaccount {:public-key member-3}}} initial-message "payload" admin)
|
||||
:now 0)]
|
||||
(testing "the message has already been received"
|
||||
(let [actual (group-chats/handle-membership-update cofx initial-message "payload" admin)]
|
||||
(testing "it noops"
|
||||
(is (=
|
||||
(get-in cofx [:db :chats chat-id])
|
||||
(get-in actual [:db :chats chat-id]))))))
|
||||
(testing "a chat we have deleted"
|
||||
(let [after-leaving-cofx (-> (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-3
|
||||
:events [{:type "member-removed"
|
||||
:clock-value 12
|
||||
:member member-3}]}]}
|
||||
"payload"
|
||||
member-3)
|
||||
(assoc-in [:db :chats chat-id :is-active] false))
|
||||
after-been-invited-again-cofx (group-chats/handle-membership-update (assoc after-leaving-cofx :now 0)
|
||||
{: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 "members-added"
|
||||
:clock-value 13
|
||||
:members [member-3]}]}]}
|
||||
"payload"
|
||||
member-2)]
|
||||
|
||||
(testing "it sets the chat active after been invited again"
|
||||
(is (get-in after-been-invited-again-cofx [:db :chats chat-id :is-active])))))
|
||||
(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]}
|
||||
{:type "name-changed"
|
||||
:clock-value 13
|
||||
:name "new-name"}]}]}
|
||||
"payload"
|
||||
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))))
|
||||
(testing "the name is updated"
|
||||
(is (= "new-name" (:name 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"]}
|
||||
{:type "member-joined"
|
||||
:clock-value 4
|
||||
:from "3"
|
||||
:member "3"}]
|
||||
expected {:name "chat-name"
|
||||
:created-at 0
|
||||
"2" {:added 1
|
||||
:admin-added 2}
|
||||
"3" {:added 3
|
||||
:joined 4}
|
||||
:admins #{"1" "2"}
|
||||
:members-joined #{"1" "3"}
|
||||
: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 "member-joined"
|
||||
:clock-value 3
|
||||
:from "2"
|
||||
:member "2"}
|
||||
{:type "admins-added"
|
||||
:clock-value 4
|
||||
:from "1"
|
||||
:members ["2"]}
|
||||
{:type "admin-removed"
|
||||
:clock-value 5
|
||||
:from "2"
|
||||
:member "2"}
|
||||
{:type "member-removed"
|
||||
:clock-value 6
|
||||
:from "2"
|
||||
:member "2"}]
|
||||
expected {:name "chat-name"
|
||||
:created-at 0
|
||||
"2" {:added 1
|
||||
:joined 3
|
||||
:admin-added 4
|
||||
:admin-removed 5
|
||||
:removed 6}
|
||||
:admins #{"1"}
|
||||
:members-joined #{"1"}
|
||||
:contacts #{"1"}}]
|
||||
(is (= expected (group-chats/build-group events)))))
|
||||
(testing "an admin removing themselves"
|
||||
(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 "member-removed"
|
||||
:clock-value 3
|
||||
:from "2"
|
||||
:member "2"}]
|
||||
expected {:name "chat-name"
|
||||
:created-at 0
|
||||
:members-joined #{"1"}
|
||||
"2" {:added 1
|
||||
:admin-added 2
|
||||
:removed 3}
|
||||
: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"
|
||||
:created-at 0
|
||||
:members-joined #{"1"}
|
||||
:name-changed-by "2"
|
||||
:name-changed-at 3
|
||||
"2" {:added 1
|
||||
:admin-added 2}
|
||||
: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 "member-joined" ; non-invited user joining
|
||||
:clock-value 2
|
||||
:from "non-invited"
|
||||
:member "non-invited"}
|
||||
{: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-joined"
|
||||
:clock-value 5
|
||||
:from "2"
|
||||
:member "2"}
|
||||
{:type "member-removed" ; can't remove an admin from the group
|
||||
:clock-value 6
|
||||
:from "1"
|
||||
:member "2"}
|
||||
{:type "members-added"
|
||||
:clock-value 7
|
||||
:from "2"
|
||||
:members ["4"]}
|
||||
{:type "member-joined"
|
||||
:clock-value 8
|
||||
:from "4"
|
||||
:member "4"}
|
||||
{:type "member-removed"
|
||||
:clock-value 9
|
||||
:from "1"
|
||||
:member "4"}
|
||||
{:type "member-joined" ; join after being removed
|
||||
:clock-value 10
|
||||
:from "4"
|
||||
:member "4"}]
|
||||
expected {:name "chat-name"
|
||||
:members-joined #{"1" "2"}
|
||||
:created-at 0
|
||||
"2" {:added 2
|
||||
:admin-added 3
|
||||
:joined 5}
|
||||
"3" {:added 4}
|
||||
"4" {:added 7
|
||||
:joined 8
|
||||
:removed 9}
|
||||
: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"
|
||||
:created-at 0
|
||||
:members-joined #{"1"}
|
||||
"2" {:added 1
|
||||
:admin-added 2}
|
||||
"3" {:added 3}
|
||||
: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 remove themselves"
|
||||
(is (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 be removed"
|
||||
(is (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 {:multiaccount {: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 [json-rpc/call (constantly nil)
|
||||
utils.clocks/send inc]
|
||||
(let [cofx {:db {:chats {chat-id {:admins #{member-1 member-2}
|
||||
:name "chat-name"
|
||||
:chat-id chat-id
|
||||
:last-clock-value 3
|
||||
: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 :multiaccount :public-key] member-3)
|
||||
chat-id)))))
|
||||
(testing "removing an admin"
|
||||
(is (= {:from member-1
|
||||
:chat-id chat-id
|
||||
:events [{:type "member-removed" :member member-1 :clock-value 4}]}
|
||||
(:group-chats/sign-membership
|
||||
(group-chats/remove
|
||||
(assoc-in cofx [:db :multiaccount :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"]
|
||||
:multiaccount {:public-key "me"}
|
||||
:chats {chat-id {:last-clock-value 1
|
||||
: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 {:multiaccount {:public-key "me"}
|
||||
:chats {chat-id {:admins #{"me"}
|
||||
:last-clock-value 1
|
||||
: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"))))))))
|
||||
|
||||
(deftest make-admin-test
|
||||
(with-redefs [utils.clocks/send inc]
|
||||
(testing "make-admin"
|
||||
(let [cofx {:db {:multiaccount {:public-key "me"}
|
||||
:chats {chat-id {:admins #{"me"}
|
||||
:last-clock-value 1
|
||||
:contacts #{"member"}
|
||||
:membership-updates [{:events [{:clock-value 1}]}]}}}}]
|
||||
(is (= {:chat-id chat-id
|
||||
:from "me"
|
||||
:events [{:type "admins-added"
|
||||
:clock-value 2
|
||||
:members ["member"]}]}
|
||||
(:group-chats/sign-membership (group-chats/make-admin cofx chat-id "member"))))))))
|
@ -20,7 +20,6 @@
|
||||
[status-im.test.ethereum.mnemonic]
|
||||
[status-im.test.ethereum.stateofus]
|
||||
[status-im.test.fleet.core]
|
||||
[status-im.test.group-chats.core]
|
||||
[status-im.test.hardwallet.core]
|
||||
[status-im.test.i18n]
|
||||
[status-im.test.mailserver.core]
|
||||
@ -93,7 +92,6 @@
|
||||
'status-im.test.ethereum.mnemonic
|
||||
'status-im.test.ethereum.stateofus
|
||||
'status-im.test.fleet.core
|
||||
'status-im.test.group-chats.core
|
||||
'status-im.test.hardwallet.core
|
||||
'status-im.test.i18n
|
||||
'status-im.test.mailserver.core
|
||||
|
Loading…
x
Reference in New Issue
Block a user