feat: added strings for groupv2 control message and components for them

pull/2963/head
Audric Ackermann 2 years ago
parent c9b2d69a73
commit dbe94f2293

@ -238,12 +238,34 @@
"autoUpdateDownloadInstructions": "Would you like to download the update?",
"leftTheGroup": "$name$ has left the group.",
"multipleLeftTheGroup": "$name$ left the group",
"updatedTheGroup": "Group updated",
"titleIsNow": "Group name is now '$name$'.",
"groupNameChange": "Group name is now <b>$name$</b>.",
"groupNameChangeFallback": "Group name updated.",
"groupAvatarChange": "Group display picture updated.",
"groupOneJoined": "<b>name</b> joined the group.",
"groupYouJoined": "<b>You</b> joined the group.",
"groupTwoJoined": "<b>$first$</b> and <b>$second$</b> joined the group.",
"groupOthersJoined": "<b>$name$</b> and <b>$count$ others</b> joined the group.",
"groupOneRemoved": "<b>name</b> was removed from the group.",
"groupYouRemoved": "<b>You</b> were removed from the group.",
"groupTwoRemoved": "<b>$first$</b> and <b>$second$</b> were removed from the group.",
"groupOthersRemoved": "<b>$name$</b> and <b>$count$ others</b> were removed from the group.",
"groupOnePromoted": "<b>name</b> was promoted to Admin.",
"groupYouPromoted": "<b>You</b> were promoted to Admin.",
"groupTwoPromoted": "<b>$first$</b> and <b>$second$</b> were promoted to Admin.",
"groupOthersPromoted": "<b>$name$</b> and <b>$count$ others</b> were promoted to Admin.",
"groupOneLeft": "<b>$name$</b> left the group.",
"groupYouLeft": "<b>You</b> left the group.",
"joinedTheGroup": "$name$ joined the group.",
"multipleJoinedTheGroup": "$name$ joined the group.",
"kickedFromTheGroup": "$name$ was removed from the group.",
"multipleKickedFromTheGroup": "$name$ were removed from the group.",
"block": "Block",
"unblock": "Unblock",
"unblocked": "Unblocked",

@ -83,8 +83,85 @@ message DataExtractionNotification {
optional uint64 timestamp = 2;
}
message GroupUpdateInviteMessage {
// @required
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required string name = 2;
required bytes memberAuthData = 3;
optional bytes profileKey = 4;
optional LokiProfile profile = 5;
// @required
required bytes adminSignature = 6;
}
message GroupUpdateDeleteMessage {
// @required
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
// @required
required bytes adminSignature = 2;
}
message GroupUpdateInfoChangeMessage {
enum Type {
NAME = 1;
AVATAR = 2;
DISAPPEARING_MESSAGES = 3;
}
// @required
required Type type = 1;
optional string updatedName = 2;
optional uint32 updatedExpiration = 3;
}
message GroupUpdateMemberChangeMessage {
enum Type {
ADDED = 1;
REMOVED = 2;
PROMOTED = 3;
}
// @required
required Type type = 1;
repeated string memberSessionIds = 2;
}
message GroupUpdatePromoteMessage {
// @required
required bytes groupIdentitySeed = 1;
}
message GroupUpdateMemberLeftMessage {
// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop)
}
message GroupUpdateInviteResponseMessage {
// @required
required bool isApproved = 1; // Whether the request was approved
optional bytes profileKey = 2;
optional LokiProfile profile = 3;
}
message GroupUpdateDeleteMemberContentMessage {
repeated string memberSessionIds = 1;
// @required
required bytes adminSignature = 2;
}
message GroupUpdateMessage {
optional GroupUpdateInviteMessage inviteMessage = 1;
optional GroupUpdateDeleteMessage deleteMessage = 2;
optional GroupUpdateInfoChangeMessage infoChangeMessage = 3;
optional GroupUpdateMemberChangeMessage memberChangeMessage = 4;
optional GroupUpdatePromoteMessage promoteMessage = 5;
optional GroupUpdateMemberLeftMessage memberLeftMessage = 6;
optional GroupUpdateInviteResponseMessage inviteResponse = 7;
optional GroupUpdateDeleteMemberContentMessage deleteMemberContent = 8;
}
message DataMessage {
@ -173,23 +250,22 @@ message DataMessage {
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Preview preview = 10;
optional Reaction reaction = 11;
optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
optional bytes profileKey = 6;
optional uint64 timestamp = 7;
optional Quote quote = 8;
repeated Preview preview = 10;
optional Reaction reaction = 11;
optional LokiProfile profile = 101;
optional OpenGroupInvitation openGroupInvitation = 102;
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
}
optional GroupUpdateMessage groupUpdateMessage = 120;}
message CallMessage {

@ -1,41 +1,149 @@
import React from 'react';
import { PubkeyType } from 'libsession_util_nodejs';
import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector';
import { arrayContainsUsOnly } from '../../../../models/message';
import { PreConditionFailed } from '../../../../session/utils/errors';
import {
PropsForGroupUpdate,
PropsForGroupUpdateType,
} from '../../../../state/ducks/conversations';
import { NotificationBubble } from './notification-bubble/NotificationBubble';
import { ReadableMessage } from './ReadableMessage';
import { arrayContainsUsOnly } from '../../../../models/message';
import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector';
import { useSelectedIsGroupV2 } from '../../../../state/selectors/selectedConversation';
import { useOurPkStr } from '../../../../state/selectors/user';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { ReadableMessage } from './ReadableMessage';
import { NotificationBubble } from './notification-bubble/NotificationBubble';
// This component is used to display group updates in the conversation view.
type IdWithName = { sessionId: PubkeyType; name: string };
const ChangeItemJoined = (added: Array<string>): string => {
function mapIdsWithNames(changed: Array<PubkeyType>, names: Array<string>): Array<IdWithName> {
if (!changed.length || !names.length) {
throw new PreConditionFailed('mapIdsWithNames needs a change');
}
if (changed.length !== names.length) {
throw new PreConditionFailed('mapIdsWithNames needs a the same length to map them together');
}
return changed.map((sessionId, index) => {
return { sessionId, name: names[index] };
});
}
/**
* When we are part of a change, we display the You first, and then others.
* This function is used to check if we are part of the list.
* - if yes: returns {weArePart: true, others: changedWithoutUs}
* - if yes: returns {weArePart: false, others: changed}
*/
function moveUsToStart(
changed: Array<IdWithName>,
us: PubkeyType
): {
sortedWithUsFirst: Array<IdWithName>;
} {
const usAt = changed.findIndex(m => m.sessionId === us);
if (usAt <= -1) {
// we are not in it
return { sortedWithUsFirst: changed };
}
const usItem = changed.at(usAt);
if (!usItem) {
throw new PreConditionFailed('"we" should have been there');
}
return { sortedWithUsFirst: [usItem, ...changed.slice(usAt, 1)] };
}
function changeOfMembersV2({
changedWithNames,
type,
us,
}: {
type: 'added' | 'promoted' | 'removed';
changedWithNames: Array<IdWithName>;
us: PubkeyType;
}): string {
const { sortedWithUsFirst } = moveUsToStart(changedWithNames, us);
if (changedWithNames.length === 0) {
throw new PreConditionFailed('change must always have an associated change');
}
const subject =
sortedWithUsFirst.length === 1 && sortedWithUsFirst[0].sessionId === us
? 'You'
: sortedWithUsFirst.length === 1
? 'One'
: sortedWithUsFirst.length === 2
? 'Two'
: 'Others';
const action =
type === 'added' ? 'Joined' : type === 'promoted' ? 'Promoted' : ('Removed' as const);
const key = `group${subject}${action}` as const;
return window.i18n(
key,
sortedWithUsFirst.map(m => m.name)
);
}
// TODO those lookups might need to be memoized
const ChangeItemJoined = (added: Array<PubkeyType>): string => {
if (!added.length) {
throw new Error('Group update add is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(added);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
return changeOfMembersV2({
changedWithNames: mapIdsWithNames(added, names),
type: 'added',
us,
});
}
const joinKey = added.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
return window.i18n(joinKey, [names.join(', ')]);
};
const ChangeItemKicked = (kicked: Array<string>): string => {
if (!kicked.length) {
throw new Error('Group update kicked is missing contacts');
const ChangeItemKicked = (removed: Array<PubkeyType>): string => {
if (!removed.length) {
throw new Error('Group update removed is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(removed);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
return changeOfMembersV2({
changedWithNames: mapIdsWithNames(removed, names),
type: 'removed',
us,
});
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(kicked);
if (arrayContainsUsOnly(kicked)) {
if (arrayContainsUsOnly(removed)) {
return window.i18n('youGotKickedFromGroup');
}
const kickedKey = kicked.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup';
const kickedKey = removed.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup';
return window.i18n(kickedKey, [names.join(', ')]);
};
const ChangeItemLeft = (left: Array<string>): string => {
const ChangeItemPromoted = (promoted: Array<PubkeyType>): string => {
if (!promoted.length) {
throw new Error('Group update promoted is missing contacts');
}
const names = useConversationsUsernameWithQuoteOrFullPubkey(promoted);
const isGroupV2 = useSelectedIsGroupV2();
const us = useOurPkStr();
if (isGroupV2) {
return changeOfMembersV2({
changedWithNames: mapIdsWithNames(promoted, names),
type: 'promoted',
us,
});
}
throw new PreConditionFailed('ChangeItemPromoted only applies to groupv2');
};
const ChangeItemLeft = (left: Array<PubkeyType>): string => {
if (!left.length) {
throw new Error('Group update remove is missing contacts');
}
@ -50,11 +158,21 @@ const ChangeItemLeft = (left: Array<string>): string => {
return window.i18n(leftKey, [names.join(', ')]);
};
const ChangeItemName = (newName: string) => {
const isGroupV2 = useSelectedIsGroupV2();
if (isGroupV2) {
return newName
? window.i18n('groupNameChange', [newName])
: window.i18n('groupNameChangeFallback');
}
return window.i18n('titleIsNow', [newName || '']);
};
const ChangeItem = (change: PropsForGroupUpdateType): string => {
const { type } = change;
switch (type) {
case 'name':
return window.i18n('titleIsNow', [change.newName || '']);
return ChangeItemName(change.newName);
case 'add':
return ChangeItemJoined(change.added);
@ -63,9 +181,8 @@ const ChangeItem = (change: PropsForGroupUpdateType): string => {
case 'kicked':
return ChangeItemKicked(change.kicked);
case 'general':
return window.i18n('updatedTheGroup');
case 'promoted':
return ChangeItemPromoted(change.promoted);
default:
assertUnreachable(type, `ChangeItem: Missing case error "${type}"`);
return '';

@ -1,5 +1,6 @@
import React from 'react';
import styled from 'styled-components';
import { SessionHtmlRenderer } from '../../../../basic/SessionHTMLRenderer';
import { SessionIcon, SessionIconType } from '../../../../icon';
const NotificationBubbleFlex = styled.div`
@ -44,8 +45,10 @@ export const NotificationBubble = (props: {
iconPadding="auto 10px"
/>
</NotificationBubbleIconContainer>
)}
<NotificationBubbleText>{notificationText}</NotificationBubbleText>
)}{' '}
<NotificationBubbleText>
<SessionHtmlRenderer html={notificationText}></SessionHtmlRenderer>
</NotificationBubbleText>
{iconType && <NotificationBubbleIconContainer />}
</NotificationBubbleFlex>
);

@ -929,7 +929,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
'conversationId' | 'source' | 'type' | 'direction' | 'received_at' | 'unread'
>
) {
let sender = UserUtils.getOurPubKeyStrFromCache();
let sender: string = UserUtils.getOurPubKeyStrFromCache();
if (this.isPublic()) {
const openGroup = OpenGroupData.getV2OpenGroupRoom(this.id);
if (openGroup && openGroup.serverPublicKey && roomHasBlindEnabled(openGroup)) {

@ -1,6 +1,8 @@
import Backbone from 'backbone';
import autoBind from 'auto-bind';
import filesize from 'filesize';
import { PubkeyType } from 'libsession_util_nodejs';
import {
cloneDeep,
debounce,
@ -14,28 +16,27 @@ import {
sortBy,
uniq,
} from 'lodash';
import filesize from 'filesize';
import { SignalService } from '../protobuf';
import { getMessageQueue } from '../session';
import { ConvoHub } from '../session/conversations';
import { DataMessage } from '../session/messages/outgoing';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
import { PubKey } from '../session/types';
import {
UserUtils,
uploadAttachmentsToFileServer,
uploadLinkPreviewToFileServer,
uploadQuoteThumbnailsToFileServer,
UserUtils,
} from '../session/utils';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
import {
DataExtractionNotificationMsg,
fillMessageAttributesWithDefaults,
MessageAttributes,
MessageAttributesOptionals,
MessageGroupUpdate,
MessageModelType,
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
fillMessageAttributesWithDefaults,
} from './messageType';
import { Data } from '../data/data';
@ -55,28 +56,27 @@ import {
uploadQuoteThumbnailsV3,
} from '../session/utils/AttachmentsV2';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { buildSyncMessage } from '../session/utils/sync/syncUtils';
import { isUsFromCache } from '../session/utils/User';
import { buildSyncMessage } from '../session/utils/sync/syncUtils';
import {
FindAndFormatContactType,
LastMessageStatusType,
MessageModelPropsWithoutConvoProps,
MessagePropsDetails,
messagesChanged,
PropsForAttachment,
PropsForExpirationTimer,
PropsForGroupInvitation,
PropsForGroupUpdate,
PropsForGroupUpdateAdd,
PropsForGroupUpdateGeneral,
PropsForGroupUpdateKicked,
PropsForGroupUpdateLeft,
PropsForGroupUpdateName,
PropsForGroupUpdatePromoted,
PropsForMessageWithoutConvoProps,
PropsForQuote,
messagesChanged,
} from '../state/ducks/conversations';
import { AttachmentTypeWithPath, isVoiceMessage } from '../types/Attachment';
import { getAttachmentMetadata } from '../types/message/initializeAttachmentMetadata';
import {
deleteExternalMessageFiles,
getAbsoluteAttachmentPath,
@ -85,6 +85,7 @@ import {
loadQuoteData,
} from '../types/MessageAttachment';
import { ReactionList } from '../types/Reaction';
import { getAttachmentMetadata } from '../types/message/initializeAttachmentMetadata';
import { roomHasBlindEnabled } from '../types/sqlSharedTypes';
import { ExpirationTimerOptions } from '../util/expiringMessages';
import { LinkPreviews } from '../util/linkPreviews';
@ -384,7 +385,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (groupUpdate.joined?.length) {
const change: PropsForGroupUpdateAdd = {
type: 'add',
added: groupUpdate.joined,
added: groupUpdate.joined as Array<PubkeyType>,
};
return { change, ...sharedProps };
}
@ -392,7 +393,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (groupUpdate.kicked?.length) {
const change: PropsForGroupUpdateKicked = {
type: 'kicked',
kicked: groupUpdate.kicked,
kicked: groupUpdate.kicked as Array<PubkeyType>,
};
return { change, ...sharedProps };
}
@ -400,11 +401,18 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (groupUpdate.left?.length) {
const change: PropsForGroupUpdateLeft = {
type: 'left',
left: groupUpdate.left,
left: groupUpdate.left as Array<PubkeyType>,
};
return { change, ...sharedProps };
}
if (groupUpdate.promoted?.length) {
const change: PropsForGroupUpdatePromoted = {
type: 'promoted',
promoted: groupUpdate.promoted as Array<PubkeyType>,
};
return { change, ...sharedProps };
}
if (groupUpdate.name) {
const change: PropsForGroupUpdateName = {
type: 'name',
@ -413,11 +421,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return { change, ...sharedProps };
}
// Just show a "Group Updated" message, not sure what was changed
const changeGeneral: PropsForGroupUpdateGeneral = {
type: 'general',
};
return { change: changeGeneral, ...sharedProps };
return null;
}
public getMessagePropStatus(): LastMessageStatusType {
@ -1162,6 +1166,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (arrayContainsUsOnly(groupUpdate.left)) {
return window.i18n('youLeftTheGroup');
}
if (groupUpdate.name) {
return window.i18n('titleIsNow', [groupUpdate.name]);
}
if (groupUpdate.left && groupUpdate.left.length === 1) {
return window.i18n('leftTheGroup', [
@ -1169,15 +1176,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
]);
}
const messages = [];
if (!groupUpdate.name && !groupUpdate.joined && !groupUpdate.kicked && !groupUpdate.kicked) {
return window.i18n('updatedTheGroup'); // Group Updated
}
if (groupUpdate.name) {
return window.i18n('titleIsNow', [groupUpdate.name]);
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = groupUpdate.joined.map(ConvoHub.use().getContactProfileNameOrShortenedPubKey);
const messages = [];
if (names.length > 1) {
messages.push(window.i18n('multipleJoinedTheGroup', [names.join(', ')]));
@ -1192,14 +1193,16 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
groupUpdate.kicked,
ConvoHub.use().getContactProfileNameOrShortenedPubKey
);
const messages = [];
if (names.length > 1) {
messages.push(window.i18n('multipleKickedFromTheGroup', [names.join(', ')]));
} else {
messages.push(window.i18n('kickedFromTheGroup', names));
}
return messages.join(' ');
}
return messages.join(' ');
return null;
}
if (this.isIncoming() && this.hasErrors()) {
return window.i18n('incomingError');

@ -1,3 +1,4 @@
import { PubkeyType } from 'libsession_util_nodejs';
import { defaultsDeep } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import {
@ -155,6 +156,7 @@ export type MessageGroupUpdate = {
left?: Array<string>;
joined?: Array<string>;
kicked?: Array<string>;
promoted?: Array<PubkeyType>;
name?: string;
};
@ -177,7 +179,6 @@ export interface MessageAttributesOptionals {
group_update?: MessageGroupUpdate;
groupInvitation?: any;
attachments?: any;
contact?: any;
conversationId: string;
errors?: any;
flags?: number;

@ -19,6 +19,7 @@ import {
getCommunityInfoFromDBValues,
getContactInfoFromDBValues,
getLegacyGroupInfoFromDBValues,
toFixedUint8ArrayOfLength,
} from '../../types/sqlSharedTypes';
import {
CLOSED_GROUP_V2_KEY_PAIRS_TABLE,
@ -34,9 +35,9 @@ import {
toSqliteBoolean,
} from '../database_utility';
import { getIdentityKeys, sqlNode } from '../sql';
import { sleepFor } from '../../session/utils/Promise';
import { SettingsKey } from '../../data/settings-key';
import { sleepFor } from '../../session/utils/Promise';
import { getIdentityKeys, sqlNode } from '../sql';
const hasDebugEnvVariable = Boolean(process.env.SESSION_DEBUG);
@ -1643,15 +1644,18 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite
const ourConvoPriority = ourConversation.priority;
// const ourConvoExpire = ourConversation.expireTimer || 0;
if (ourDbProfileUrl && !isEmpty(ourDbProfileKey)) {
userProfileWrapper.setUserInfo(
ourDbName,
ourConvoPriority,
{
url: ourDbProfileUrl,
key: ourDbProfileKey,
}
// ourConvoExpire,
);
if (ourDbProfileKey.length === 32) {
const ourKeyFixedLen = toFixedUint8ArrayOfLength(ourDbProfileKey, 32);
userProfileWrapper.setUserInfo(
ourDbName,
ourConvoPriority,
{
url: ourDbProfileUrl,
key: ourKeyFixedLen.buffer, // TODO make this use the fixed length array
}
// ourConvoExpire,
);
}
}
insertContactIntoContactWrapper(

@ -219,14 +219,17 @@ async function handleUserProfileUpdate(result: IncomingUserResult) {
await window.setSettingValue(SettingsKey.hasBlindedMsgRequestsEnabled, newBlindedMsgRequest); // this does the dispatch to redux
}
const picUpdate = !isEmpty(updateUserInfo.key) && !isEmpty(updateUserInfo.url);
const picUpdate =
!isEmpty(updateUserInfo.key) &&
!isEmpty(updateUserInfo.url) &&
updateUserInfo.key.length === 32;
// NOTE: if you do any changes to the settings of a user which are synced, it should be done above the `updateOurProfileViaLibSession` call
await updateOurProfileViaLibSession(
result.latestEnvelopeTimestamp,
updateUserInfo.name,
picUpdate ? updateUserInfo.url : null,
picUpdate ? updateUserInfo.key : null,
picUpdate ? updateUserInfo.key : null, // TODO make the whole logic of handling profileKeys used the UInt8ArrayFixedLength
updateUserInfo.priority
);
@ -629,9 +632,10 @@ async function handleSingleGroupUpdate({
// dump is always empty when creating a new groupInfo
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEdKeypair.privKeyBytes, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEdKeypair.privKeyBytes, 64).buffer,
groupEd25519Secretkey: groupInWrapper.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(HexString.fromHexString(groupPk.slice(2)), 32),
groupEd25519Pubkey: toFixedUint8ArrayOfLength(HexString.fromHexString(groupPk.slice(2)), 32)
.buffer,
});
} catch (e) {
window.log.warn(

@ -1,4 +1,4 @@
import { FixedSizeUint8Array, GroupPubkeyType } from 'libsession_util_nodejs';
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { toFixedUint8ArrayOfLength } from '../../../types/sqlSharedTypes';
import { getSodiumRenderer } from '../../crypto';
@ -72,11 +72,11 @@ type SnodeSigParamsShared = {
type SnodeSigParamsAdminGroup = SnodeSigParamsShared & {
groupPk: GroupPubkeyType;
privKey: Uint8Array; // our ed25519 key when we are signing with our pubkey
privKey: Uint8Array; // len 64
};
type SnodeSigParamsUs = SnodeSigParamsShared & {
pubKey: string;
privKey: FixedSizeUint8Array<64>;
privKey: Uint8Array; // len 64
};
function isSigParamsForGroupAdmin(
@ -95,7 +95,7 @@ async function getSnodeSignatureShared(params: SnodeSigParamsAdminGroup | SnodeS
try {
const message = new Uint8Array(verificationData);
const sodium = await getSodiumRenderer();
const signature = sodium.crypto_sign_detached(message, params.privKey as Uint8Array);
const signature = sodium.crypto_sign_detached(message, params.privKey);
const signatureBase64 = fromUInt8ArrayToBase64(signature);
if (isSigParamsForGroupAdmin(params)) {
return {
@ -134,7 +134,7 @@ async function getSnodeSignatureParamsUs({
pubKey: UserUtils.getOurPubKeyStrFromCache(),
method,
namespace,
privKey: lengthCheckedPrivKey,
privKey: lengthCheckedPrivKey.buffer,
});
const us = UserUtils.getOurPubKeyStrFromCache();
@ -152,7 +152,7 @@ async function getSnodeGroupSignatureParams({
namespace,
}: {
groupPk: GroupPubkeyType;
groupIdentityPrivKey: FixedSizeUint8Array<64>;
groupIdentityPrivKey: Uint8Array; // len 64
namespace: SnodeNamespacesGroup;
method: 'retrieve' | 'store';
}): Promise<SnodeGroupSignatureResult> {
@ -174,7 +174,7 @@ async function generateUpdateExpirySignature({
}: WithMessagesHashes &
WithShortenOrExtend &
WithTimestamp & {
ed25519Privkey: Uint8Array | FixedSizeUint8Array<64>;
ed25519Privkey: Uint8Array; // len 64
ed25519Pubkey: string;
}): Promise<{ signature: string; pubkey: string }> {
// "expire" || ShortenOrExtend || expiry || messages[0] || ... || messages[N]
@ -184,7 +184,7 @@ async function generateUpdateExpirySignature({
const sodium = await getSodiumRenderer();
const signature = sodium.crypto_sign_detached(message, ed25519Privkey as Uint8Array);
const signature = sodium.crypto_sign_detached(message, ed25519Privkey);
const signatureBase64 = fromUInt8ArrayToBase64(signature);
if (isEmpty(signatureBase64) || isEmpty(ed25519Pubkey)) {
@ -216,7 +216,7 @@ async function generateUpdateExpiryOurSignature({
messagesHashes,
shortenOrExtend,
timestamp,
ed25519Privkey: edKeyPrivBytes,
ed25519Privkey: toFixedUint8ArrayOfLength(edKeyPrivBytes, 64).buffer,
ed25519Pubkey: ourEd25519Key.pubKey,
});
}
@ -231,7 +231,7 @@ async function generateUpdateExpiryGroupSignature({
WithShortenOrExtend &
WithTimestamp & {
groupPk: GroupPubkeyType;
groupPrivKey: FixedSizeUint8Array<64>;
groupPrivKey: Uint8Array; // len 64
}) {
if (isEmpty(groupPrivKey) || isEmpty(groupPk)) {
throw new PreConditionFailed(

@ -0,0 +1,31 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../protobuf';
import { DataMessage } from '../../DataMessage';
import { MessageParams } from '../../Message';
export interface GroupUpdateMessageParams extends MessageParams {
groupPk: GroupPubkeyType;
}
export abstract class GroupUpdateMessage extends DataMessage {
public readonly groupPk: GroupUpdateMessageParams['groupPk'];
constructor(params: GroupUpdateMessageParams) {
super(params);
this.groupPk = params.groupPk;
if (!this.groupPk || this.groupPk.length === 0) {
throw new Error('groupPk must be set');
}
}
protected abstract updateProto(): SignalService.GroupUpdateMessage;
public dataProto(): SignalService.DataMessage {
const groupUpdateMessage = this.updateProto();
return new SignalService.DataMessage({ groupUpdateMessage });
}
public abstract isFor1o1Swarm(): boolean;
public abstract isForGroupSwarm(): boolean;
}

@ -0,0 +1,45 @@
import { PubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
type Params = GroupUpdateMessageParams & {
memberSessionIds: Array<PubkeyType>;
adminSignature: Uint8Array; // this is a signature of `"DELETE_CONTENT" || timestamp || sessionId[0] || ... || sessionId[N]`
};
/**
* GroupUpdateDeleteMemberContentMessage is sent as a message to group's swarm.
*/
export class GroupUpdateDeleteMemberContentMessage extends GroupUpdateMessage {
public readonly memberSessionIds: Params['memberSessionIds'];
public readonly adminSignature: Params['adminSignature'];
constructor(params: Params) {
super(params);
this.adminSignature = params.adminSignature;
this.memberSessionIds = params.memberSessionIds;
if (isEmpty(this.memberSessionIds)) {
throw new Error('GroupUpdateDeleteMemberContentMessage needs members in list');
}
}
protected updateProto(): SignalService.GroupUpdateMessage {
const deleteMemberContent = new SignalService.GroupUpdateDeleteMemberContentMessage({
adminSignature: this.adminSignature,
memberSessionIds: this.memberSessionIds,
});
return new SignalService.GroupUpdateMessage({
deleteMemberContent,
});
}
public isForGroupSwarm(): boolean {
return true;
}
public isFor1o1Swarm(): boolean {
return false;
}
}

@ -0,0 +1,81 @@
import { isEmpty, isFinite } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
type NameChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.NAME;
updatedName: string;
};
type AvatarChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.AVATAR;
};
type DisappearingMessageChangeParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES;
updatedExpirationSeconds: number;
};
/**
* GroupUpdateInfoChangeMessage is sent as a message to group's swarm.
*/
export class GroupUpdateInfoChangeMessage extends GroupUpdateMessage {
public readonly typeOfChange: SignalService.GroupUpdateInfoChangeMessage.Type;
public readonly updatedName: string = '';
public readonly updatedExpirationSeconds: number = 0;
constructor(params: NameChangeParams | AvatarChangeParams | DisappearingMessageChangeParams) {
super(params);
const types = SignalService.GroupUpdateInfoChangeMessage.Type;
this.typeOfChange = params.typeOfChange;
switch (params.typeOfChange) {
case types.NAME: {
if (isEmpty(params.updatedName)) {
throw new Error('A group needs a name');
}
this.updatedName = params.updatedName;
break;
}
case types.AVATAR:
// nothing to do for avatar
break;
case types.DISAPPEARING_MESSAGES: {
if (!isFinite(params.updatedExpirationSeconds) || params.updatedExpirationSeconds < 0) {
throw new Error('Invalid disappearing message timer. Must be finite and >=0');
}
this.updatedExpirationSeconds = params.updatedExpirationSeconds;
break;
}
default:
break;
}
}
protected updateProto(): SignalService.GroupUpdateMessage {
const infoChangeMessage = new SignalService.GroupUpdateInfoChangeMessage({
type: this.typeOfChange,
});
if (this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.NAME) {
infoChangeMessage.updatedName = this.updatedName;
}
if (
this.typeOfChange === SignalService.GroupUpdateInfoChangeMessage.Type.DISAPPEARING_MESSAGES
) {
infoChangeMessage.updatedExpiration = this.updatedExpirationSeconds;
}
return new SignalService.GroupUpdateMessage({
infoChangeMessage,
});
}
public isForGroupSwarm(): boolean {
return true;
}
public isFor1o1Swarm(): boolean {
return false;
}
}

@ -0,0 +1,46 @@
import { SignalService } from '../../../../../../protobuf';
import { getOurProfile } from '../../../../../utils/User';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
type Params = GroupUpdateMessageParams & {
isApproved: boolean;
};
/**
* GroupUpdateInviteResponseMessage is sent to the group's swarm.
* Our pubkey, as the leaving member is part of the encryption of libsession for the new groups
*
*/
export class GroupUpdateInviteResponseMessage extends GroupUpdateMessage {
public readonly isApproved: Params['isApproved'];
constructor(params: Params) {
super(params);
this.isApproved = params.isApproved;
}
protected updateProto(): SignalService.GroupUpdateMessage {
const ourProfile = getOurProfile();
const inviteResponse = new SignalService.GroupUpdateInviteResponseMessage({
isApproved: true,
profileKey: ourProfile?.profileKey,
profile: ourProfile
? {
displayName: ourProfile.displayName,
profilePicture: ourProfile.avatarPointer,
}
: undefined,
});
return new SignalService.GroupUpdateMessage({
inviteResponse,
});
}
public isForGroupSwarm(): boolean {
return true;
}
public isFor1o1Swarm(): boolean {
return false;
}
}

@ -0,0 +1,82 @@
import { PubkeyType } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { SignalService } from '../../../../../../protobuf';
import { assertUnreachable } from '../../../../../../types/sqlSharedTypes';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
type MembersAddedMessageParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.ADDED;
added: Array<PubkeyType>;
};
type MembersRemovedMessageParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.REMOVED;
removed: Array<PubkeyType>;
};
type MembersPromotedMessageParams = GroupUpdateMessageParams & {
typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type.PROMOTED;
promoted: Array<PubkeyType>;
};
/**
* GroupUpdateInfoChangeMessage is sent to the group's swarm.
*/
export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage {
public readonly typeOfChange: SignalService.GroupUpdateMemberChangeMessage.Type;
public readonly memberSessionIds: Array<PubkeyType> = []; // added, removed, promoted based on the type.
constructor(
params: MembersAddedMessageParams | MembersRemovedMessageParams | MembersPromotedMessageParams
) {
super(params);
const { Type } = SignalService.GroupUpdateMemberChangeMessage;
const { typeOfChange } = params;
this.typeOfChange = typeOfChange;
switch (typeOfChange) {
case Type.ADDED: {
if (isEmpty(params.added)) {
throw new Error('added members list cannot be empty');
}
this.memberSessionIds = params.added;
break;
}
case Type.REMOVED: {
if (isEmpty(params.removed)) {
throw new Error('removed members list cannot be empty');
}
this.memberSessionIds = params.removed;
break;
}
case Type.PROMOTED: {
if (isEmpty(params.promoted)) {
throw new Error('promoted members list cannot be empty');
}
this.memberSessionIds = params.promoted;
break;
}
default:
assertUnreachable(typeOfChange, 'unhandled switch case');
}
}
protected updateProto(): SignalService.GroupUpdateMessage {
const memberChangeMessage = new SignalService.GroupUpdateMemberChangeMessage({
type: this.typeOfChange,
memberSessionIds: this.memberSessionIds,
});
return new SignalService.GroupUpdateMessage({
memberChangeMessage,
});
}
public isForGroupSwarm(): boolean {
return true;
}
public isFor1o1Swarm(): boolean {
return false;
}
}

@ -0,0 +1,24 @@
import { SignalService } from '../../../../../../protobuf';
import { GroupUpdateMessage } from '../GroupUpdateMessage';
/**
* GroupUpdateMemberLeftMessage is sent to the group's swarm.
* Our pubkey, as the leaving member is part of the encryption of libsession for the new groups
*
*/
export class GroupUpdateMemberLeftMessage extends GroupUpdateMessage {
protected updateProto(): SignalService.GroupUpdateMessage {
const memberLeftMessage = new SignalService.GroupUpdateMemberLeftMessage({});
return new SignalService.GroupUpdateMessage({
memberLeftMessage,
});
}
public isForGroupSwarm(): boolean {
return true;
}
public isFor1o1Swarm(): boolean {
return false;
}
}

@ -0,0 +1,34 @@
import { SignalService } from '../../../../../../protobuf';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
interface Params extends GroupUpdateMessageParams {
adminSignature: Uint8Array; // this is a signature of `"DELETE" || sessionId || timestamp `
}
/**
* GroupUpdateDeleteMessage is sent as a 1o1 message to the recipient, not through the group's swarm.
*/
export class GroupUpdateDeleteMessage extends GroupUpdateMessage {
public readonly adminSignature: Params['adminSignature'];
constructor(params: Params) {
super(params);
this.adminSignature = params.adminSignature;
}
protected updateProto(): SignalService.GroupUpdateMessage {
const deleteMessage = new SignalService.GroupUpdateDeleteMessage({
groupSessionId: this.groupPk,
adminSignature: this.adminSignature,
});
return new SignalService.GroupUpdateMessage({ deleteMessage });
}
public isForGroupSwarm(): boolean {
return false;
}
public isFor1o1Swarm(): boolean {
return true;
}
}

@ -0,0 +1,53 @@
import { SignalService } from '../../../../../../protobuf';
import { UserUtils } from '../../../../../utils';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
interface Params extends GroupUpdateMessageParams {
groupName: string;
adminSignature: Uint8Array; // this is a signature of `"INVITE" || inviteeSessionId || timestamp`
memberAuthData: Uint8Array;
}
/**
* GroupUpdateInviteMessage is sent as a 1o1 message to the recipient, not through the group's swarm.
*/
export class GroupUpdateInviteMessage extends GroupUpdateMessage {
public readonly groupName: Params['groupName'];
public readonly adminSignature: Params['adminSignature'];
public readonly memberAuthData: Params['memberAuthData'];
constructor(params: Params) {
super({
timestamp: params.timestamp,
identifier: params.identifier,
groupPk: params.groupPk,
});
this.groupName = params.groupName;
this.adminSignature = params.adminSignature;
this.memberAuthData = params.memberAuthData;
}
protected updateProto(): SignalService.GroupUpdateMessage {
const ourProfile = UserUtils.getOurProfile();
const inviteMessage = new SignalService.GroupUpdateInviteMessage({
groupSessionId: this.groupPk,
name: this.groupName,
adminSignature: this.adminSignature,
memberAuthData: this.memberAuthData,
profile: ourProfile
? { displayName: ourProfile.displayName, profilePicture: ourProfile.avatarPointer }
: undefined,
profileKey: ourProfile?.profileKey,
});
return new SignalService.GroupUpdateMessage({ inviteMessage });
}
public isForGroupSwarm(): boolean {
return false;
}
public isFor1o1Swarm(): boolean {
return true;
}
}

@ -0,0 +1,39 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { SignalService } from '../../../../../../protobuf';
import { GroupUpdateMessage, GroupUpdateMessageParams } from '../GroupUpdateMessage';
interface Params extends GroupUpdateMessageParams {
groupPk: GroupPubkeyType;
groupIdentitySeed: Uint8Array;
}
/**
* GroupUpdateDeleteMessage is sent as a 1o1 message to the recipient, not through the group's swarm.
*/
export class GroupUpdatePromoteMessage extends GroupUpdateMessage {
public readonly groupIdentitySeed: Params['groupIdentitySeed'];
constructor(params: Params) {
super(params);
this.groupIdentitySeed = params.groupIdentitySeed;
if (!this.groupIdentitySeed || this.groupIdentitySeed.length !== 32) {
throw new Error('groupIdentitySeed must be set');
}
}
protected updateProto(): SignalService.GroupUpdateMessage {
const promoteMessage = new SignalService.GroupUpdatePromoteMessage({
groupIdentitySeed: this.groupIdentitySeed,
});
return new SignalService.GroupUpdateMessage({ promoteMessage });
}
public isForGroupSwarm(): boolean {
return false;
}
public isFor1o1Swarm(): boolean {
return true;
}
}

@ -1,3 +1,4 @@
import { PubkeyType } from 'libsession_util_nodejs';
import _ from 'lodash';
import { UserUtils } from '.';
import { Data } from '../../data/data';
@ -35,13 +36,13 @@ export function isUsFromCache(pubKey: string | PubKey | undefined): boolean {
/**
* Returns the public key of this current device as a STRING, or throws an error
*/
export function getOurPubKeyStrFromCache(): string {
export function getOurPubKeyStrFromCache(): PubkeyType {
const ourNumber = getOurPubKeyStrFromStorage();
if (!ourNumber) {
throw new Error('ourNumber is not set');
}
return ourNumber;
return ourNumber as PubkeyType;
}
/**

@ -1,11 +1,12 @@
import { isEmpty } from 'lodash';
import { UserUtils } from '..';
import { SettingsKey } from '../../../data/settings-key';
import { CONVERSATION_PRIORITIES } from '../../../models/conversationAttributes';
import { toFixedUint8ArrayOfLength } from '../../../types/sqlSharedTypes';
import { Storage } from '../../../util/storage';
import { UserConfigWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { ConvoHub } from '../../conversations';
import { fromHexToArray } from '../String';
import { CONVERSATION_PRIORITIES } from '../../../models/conversationAttributes';
import { Storage } from '../../../util/storage';
import { SettingsKey } from '../../../data/settings-key';
async function insertUserProfileIntoWrapper(convoId: string) {
if (!isUserProfileToStoreInWrapper(convoId)) {
@ -32,15 +33,18 @@ async function insertUserProfileIntoWrapper(convoId: string) {
);
// const expirySeconds = ourConvo.get('expireTimer') || 0;
if (dbProfileUrl && !isEmpty(dbProfileKey)) {
await UserConfigWrapperActions.setUserInfo(
dbName,
priority,
{
url: dbProfileUrl,
key: dbProfileKey,
}
// expirySeconds
);
if (dbProfileKey.length === 32) {
const fixedLen = toFixedUint8ArrayOfLength(dbProfileKey, 32);
await UserConfigWrapperActions.setUserInfo(
dbName,
priority,
{
url: dbProfileUrl,
key: fixedLen.buffer, // TODO make this use the fixed length array
}
// expirySeconds
);
}
} else {
await UserConfigWrapperActions.setUserInfo(dbName, priority, null); // expirySeconds
}

@ -1,5 +1,6 @@
/* eslint-disable no-restricted-syntax */
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PubkeyType } from 'libsession_util_nodejs';
import { omit, toNumber } from 'lodash';
import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox';
import { QuotedAttachmentType } from '../../components/conversation/message/message-content/quote/Quote';
@ -83,23 +84,24 @@ export type PropsForExpirationTimer = {
receivedAt: number | undefined;
};
export type PropsForGroupUpdateGeneral = {
type: 'general';
};
export type PropsForGroupUpdateAdd = {
type: 'add';
added: Array<string>;
added: Array<PubkeyType>;
};
export type PropsForGroupUpdateKicked = {
type: 'kicked';
kicked: Array<string>;
kicked: Array<PubkeyType>;
};
export type PropsForGroupUpdatePromoted = {
type: 'promoted';
promoted: Array<PubkeyType>;
};
export type PropsForGroupUpdateLeft = {
type: 'left';
left: Array<string>;
left: Array<PubkeyType>;
};
export type PropsForGroupUpdateName = {
@ -108,9 +110,9 @@ export type PropsForGroupUpdateName = {
};
export type PropsForGroupUpdateType =
| PropsForGroupUpdateGeneral
| PropsForGroupUpdateAdd
| PropsForGroupUpdateKicked
| PropsForGroupUpdatePromoted
| PropsForGroupUpdateName
| PropsForGroupUpdateLeft;

@ -89,9 +89,9 @@ const initNewGroupInWrapper = createAsyncThunk(
// dump is always empty when creating a new groupInfo
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64).buffer,
groupEd25519Secretkey: newGroup.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32),
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32).buffer,
});
for (let index = 0; index < uniqMembers.length; index++) {
@ -177,9 +177,9 @@ const handleUserGroupUpdate = createAsyncThunk(
try {
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64).buffer,
groupEd25519Secretkey: userGroup.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32),
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32).buffer,
});
} catch (e) {
window.log.warn(`failed to init metawrapper ${groupPk}`);
@ -246,9 +246,10 @@ const loadMetaDumpsFromDB = createAsyncThunk(
window.log.debug('loadMetaDumpsFromDB initing from metagroup dump', variant);
await MetaGroupWrapperActions.init(groupPk, {
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd25519Pubkey, 32),
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd25519Pubkey, 32).buffer,
groupEd25519Secretkey: foundInUserWrapper?.secretKey || null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(ed25519KeyPairBytes.privKeyBytes, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(ed25519KeyPairBytes.privKeyBytes, 64)
.buffer,
metaDumped: data,
});

@ -124,6 +124,20 @@ const getSelectedConversationIsGroup = (state: StateType): boolean => {
return selected.type ? isOpenOrClosedGroup(selected.type) : false;
};
/**
* Returns true if the current conversation selected is a group conversation.
* Returns false if the current conversation selected is not a group conversation, or none are selected
*/
const getSelectedConversationIsGroupV2 = (state: StateType): boolean => {
const selected = getSelectedConversation(state);
if (!selected || !selected.type) {
return false;
}
return selected.type
? selected.type === ConversationTypeEnum.GROUPV2 && PubKey.isClosedGroupV2(selected.id)
: false;
};
/**
* Returns true if the current conversation selected is a closed group and false otherwise.
*/
@ -179,6 +193,9 @@ export function useSelectedConversationKey() {
export function useSelectedIsGroup() {
return useSelector(getSelectedConversationIsGroup);
}
export function useSelectedIsGroupV2() {
return useSelector(getSelectedConversationIsGroupV2);
}
export function useSelectedIsPublic() {
return useSelector(getSelectedConversationIsPublic);

@ -1,16 +1,17 @@
import { createSelector } from '@reduxjs/toolkit';
import { PubkeyType } from 'libsession_util_nodejs';
import { useSelector } from 'react-redux';
import { LocalizerType } from '../../types/Util';
import { StateType } from '../reducer';
import { UserStateType } from '../ducks/user';
import { StateType } from '../reducer';
export const getUser = (state: StateType): UserStateType => state.user;
export const getOurNumber = createSelector(
getUser,
(state: UserStateType): string => state.ourNumber
(state: UserStateType): PubkeyType => state.ourNumber as PubkeyType
);
export const getIntl = createSelector(getUser, (): LocalizerType => window.i18n);

@ -1,18 +1,19 @@
import * as crypto from 'crypto';
/* eslint-disable import/order */
import chai, { expect } from 'chai';
import Sinon, * as sinon from 'sinon';
import chaiBytes from 'chai-bytes';
import * as crypto from 'crypto';
import Sinon, * as sinon from 'sinon';
import { SignalService } from '../../../../protobuf';
import { concatUInt8Array, getSodiumRenderer, MessageEncrypter } from '../../../../session/crypto';
import { TestUtils } from '../../../test-utils';
import { SignalService } from '../../../../protobuf';
import { StringUtils, UserUtils } from '../../../../session/utils';
import { SessionKeyPair } from '../../../../receiver/keypairs';
import { addMessagePadding } from '../../../../session/crypto/BufferPadding';
import { PubKey } from '../../../../session/types';
import { fromHex, toHex } from '../../../../session/utils/String';
import { addMessagePadding } from '../../../../session/crypto/BufferPadding';
import { SessionKeyPair } from '../../../../receiver/keypairs';
export const TEST_identityKeyPair: SessionKeyPair = {
pubKey: new Uint8Array([
@ -32,7 +33,7 @@ export const TEST_identityKeyPair: SessionKeyPair = {
chai.use(chaiBytes);
describe('MessageEncrypter', () => {
const ourNumber = '0123456789abcdef';
const ourNumber = TestUtils.generateFakePubKeyStr();
const ourUserEd25516Keypair = {
pubKey: '37e1631b002de498caf7c5c1712718bde7f257c6dadeed0c21abf5e939e6c309',
privKey:

@ -9,18 +9,14 @@ import { SnodeSignature } from '../../../../session/apis/snode_api/snodeSignatur
import { concatUInt8Array } from '../../../../session/crypto';
import { UserUtils } from '../../../../session/utils';
import { fromBase64ToArray, fromHexToArray } from '../../../../session/utils/String';
import { toFixedUint8ArrayOfLength } from '../../../../types/sqlSharedTypes';
use(chaiAsPromised);
const validGroupPk = '03eef710fcaaa73fd50c4311333f5c496e0fdbbe9e8a70fdfa95e7ec62d5032f5c';
const privKeyUint = toFixedUint8ArrayOfLength(
concatUInt8Array(
fromHexToArray('cd8488c39bf9972739046d627e7796b2bc0e38e2fa99fc4edd59205c28f2cdb1'),
fromHexToArray(validGroupPk.slice(2))
),
64
);
const privKeyUint = concatUInt8Array(
fromHexToArray('cd8488c39bf9972739046d627e7796b2bc0e38e2fa99fc4edd59205c28f2cdb1'),
fromHexToArray(validGroupPk.slice(2))
); // len 64
const userEd25519Keypair = {
pubKey: '37e1631b002de498caf7c5c1712718bde7f257c6dadeed0c21abf5e939e6c309',

@ -4,8 +4,8 @@ import {
MetaGroupWrapperNode,
UserGroupsWrapperNode,
} from 'libsession_util_nodejs';
import Sinon from 'sinon';
import { range } from 'lodash';
import Sinon from 'sinon';
import { HexString } from '../../../../node/hexStrings';
import { toFixedUint8ArrayOfLength } from '../../../../types/sqlSharedTypes';
import { TestUtils } from '../../../test-utils';
@ -47,10 +47,10 @@ describe('libsession_metagroup', () => {
groupEd25519Pubkey: toFixedUint8ArrayOfLength(
HexString.fromHexString(groupCreated.pubkeyHex.slice(2)),
32
),
).buffer,
groupEd25519Secretkey: groupCreated.secretKey,
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(us.ed25519KeyPair.privateKey, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(us.ed25519KeyPair.privateKey, 64).buffer,
});
member = TestUtils.generateFakePubKeyStr();
member2 = TestUtils.generateFakePubKeyStr();
@ -264,10 +264,10 @@ describe('libsession_metagroup', () => {
groupEd25519Pubkey: toFixedUint8ArrayOfLength(
HexString.fromHexString(groupCreated.pubkeyHex.slice(2)),
32
),
).buffer,
groupEd25519Secretkey: groupCreated.secretKey,
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(us.ed25519KeyPair.privateKey, 64),
userEd25519Secretkey: toFixedUint8ArrayOfLength(us.ed25519KeyPair.privateKey, 64).buffer,
});
// mark current user as admin

@ -1,19 +1,20 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-unused-expressions */
import chai, { expect } from 'chai';
import Sinon, { useFakeTimers } from 'sinon';
import { noop } from 'lodash';
import chaiAsPromised from 'chai-as-promised';
import { noop } from 'lodash';
import Sinon, { useFakeTimers } from 'sinon';
import { Reactions } from '../../../../util/reactions';
import { Data } from '../../../../data/data';
import { DEFAULT_RECENT_REACTS } from '../../../../session/constants';
import { Reactions } from '../../../../util/reactions';
import * as Storage from '../../../../util/storage';
import { generateFakeIncomingPrivateMessage, stubWindowLog } from '../../../test-utils/utils';
import { DEFAULT_RECENT_REACTS } from '../../../../session/constants';
import { UserUtils } from '../../../../session/utils';
import { SignalService } from '../../../../protobuf';
import { MessageCollection } from '../../../../models/message';
import { SignalService } from '../../../../protobuf';
import { UserUtils } from '../../../../session/utils';
import { TestUtils } from '../../../test-utils';
chai.use(chaiAsPromised as any);
@ -21,7 +22,7 @@ describe('ReactionMessage', () => {
stubWindowLog();
let clock: Sinon.SinonFakeTimers;
const ourNumber = '0123456789abcdef';
const ourNumber = TestUtils.generateFakePubKeyStr();
const originalMessage = generateFakeIncomingPrivateMessage();
originalMessage.set('sent_at', Date.now());

@ -13,6 +13,7 @@ import chaiAsPromised from 'chai-as-promised';
import { describe } from 'mocha';
import Sinon, * as sinon from 'sinon';
import { PubkeyType } from 'libsession_util_nodejs';
import { ContentMessage } from '../../../../session/messages/outgoing';
import { ClosedGroupMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupMessage';
import { MessageSender } from '../../../../session/sending';
@ -34,7 +35,7 @@ const { expect } = chai;
describe('MessageQueue', () => {
// Initialize new stubbed cache
const ourDevice = TestUtils.generateFakePubKey();
const ourNumber = ourDevice.key;
const ourNumber = ourDevice.key as PubkeyType;
// Initialize new stubbed queue
let pendingMessageCache: PendingMessageCacheStub;

@ -1,5 +1,6 @@
import * as crypto from 'crypto';
import { expect } from 'chai';
// eslint-disable-next-line import/order
import * as crypto from 'crypto';
import _ from 'lodash';
import Sinon, * as sinon from 'sinon';
import { SignalService } from '../../../../protobuf';
@ -16,10 +17,10 @@ import { OnionV4 } from '../../../../session/onions/onionv4';
import { MessageSender } from '../../../../session/sending';
import { PubKey, RawMessage } from '../../../../session/types';
import { MessageUtils, UserUtils } from '../../../../session/utils';
import { fromBase64ToArrayBuffer } from '../../../../session/utils/String';
import { TestUtils } from '../../../test-utils';
import { stubCreateObjectUrl, stubData, stubUtilWorker } from '../../../test-utils/utils';
import { TEST_identityKeyPair } from '../crypto/MessageEncrypter_test';
import { fromBase64ToArrayBuffer } from '../../../../session/utils/String';
describe('MessageSender', () => {
afterEach(() => {
@ -38,7 +39,7 @@ describe('MessageSender', () => {
});
describe('send', () => {
const ourNumber = '0123456789abcdef';
const ourNumber = TestUtils.generateFakePubKeyStr();
let sessionMessageAPISendStub: sinon.SinonStub<any>;
let encryptStub: sinon.SinonStub<[PubKey, Uint8Array, SignalService.Envelope.Type]>;

@ -2,7 +2,12 @@ import chai from 'chai';
import { describe } from 'mocha';
import Sinon, * as sinon from 'sinon';
import { GroupPubkeyType, LegacyGroupInfo, UserGroupsGet } from 'libsession_util_nodejs';
import {
GroupPubkeyType,
LegacyGroupInfo,
PubkeyType,
UserGroupsGet,
} from 'libsession_util_nodejs';
import { ConversationModel, Convo } from '../../../../models/conversation';
import { ConversationTypeEnum } from '../../../../models/conversationAttributes';
import { SnodePool, getSwarmPollingInstance } from '../../../../session/apis/snode_api';
@ -38,7 +43,7 @@ function stubWithGroups(pubkeys: Array<GroupPubkeyType>) {
describe('SwarmPolling:pollForAllKeys', () => {
const ourPubkey = TestUtils.generateFakePubKey();
const ourNumber = ourPubkey.key;
const ourNumber = ourPubkey.key as PubkeyType;
let pollOnceForKeySpy: Sinon.SinonSpy<
Parameters<SwarmPolling['pollOnceForKey']>,

@ -13,8 +13,7 @@ import { stubData } from '../../../test-utils/utils';
describe('getPollingDetails', () => {
// Initialize new stubbed cache
const ourPubkey = TestUtils.generateFakePubKey();
const ourNumber = ourPubkey.key;
const ourNumber = TestUtils.generateFakePubKeyStr();
let swarmPolling: SwarmPolling;
@ -54,7 +53,7 @@ describe('getPollingDetails', () => {
swarmPolling.resetSwarmPolling();
const fn = async () =>
swarmPolling.getPollingDetails([{ pubkey: PubKey.cast(ourPubkey), lastPolledTimestamp: 0 }]);
swarmPolling.getPollingDetails([{ pubkey: PubKey.cast(ourNumber), lastPolledTimestamp: 0 }]);
await expect(fn()).to.be.rejectedWith('');
});

@ -17,11 +17,11 @@ export function generateFakePubKey(): PubKey {
return new PubKey(pubkeyString);
}
export function generateFakePubKeyStr(): string {
export function generateFakePubKeyStr(): PubkeyType {
// Generates a mock pubkey for testing
const numBytes = PubKey.PUBKEY_LEN / 2 - 1;
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
const pubkeyString = `05${hexBuffer}`;
const pubkeyString: PubkeyType = `05${hexBuffer}`;
return pubkeyString;
}

@ -1,5 +1,6 @@
import { expect } from 'chai';
import Sinon from 'sinon';
import { Convo } from '../../models/conversation';
import { BlockedNumberController } from '../../util/blockedNumberController';
import { TestUtils } from '../test-utils';
@ -51,6 +52,8 @@ describe('BlockedNumberController', () => {
describe('block', () => {
it('should block the user', async () => {
Sinon.stub(Convo, 'commitConversationAndRefreshWrapper').resolves();
const other = TestUtils.generateFakePubKey();
await BlockedNumberController.block(other);

@ -189,8 +189,25 @@ export type LocalizerKeys =
| 'getStarted'
| 'goToReleaseNotes'
| 'goToSupportPage'
| 'groupAvatarChange'
| 'groupMembers'
| 'groupNameChange'
| 'groupNameChangeFallback'
| 'groupNamePlaceholder'
| 'groupOneJoined'
| 'groupOneLeft'
| 'groupOnePromoted'
| 'groupOneRemoved'
| 'groupOthersJoined'
| 'groupOthersPromoted'
| 'groupOthersRemoved'
| 'groupTwoJoined'
| 'groupTwoPromoted'
| 'groupTwoRemoved'
| 'groupYouJoined'
| 'groupYouLeft'
| 'groupYouPromoted'
| 'groupYouRemoved'
| 'helpSettingsTitle'
| 'helpUsTranslateSession'
| 'hideBanner'
@ -483,7 +500,6 @@ export type LocalizerKeys =
| 'unpinConversation'
| 'unreadMessages'
| 'updateGroupDialogTitle'
| 'updatedTheGroup'
| 'userAddedToModerators'
| 'userBanFailed'
| 'userBanned'

@ -3,10 +3,10 @@
// eslint-disable-next-line camelcase
import {
ContactInfoSet,
FixedSizeUint8Array,
GroupPubkeyType,
LegacyGroupInfo,
LegacyGroupMemberInfo,
Uint8ArrayFixedLength,
} from 'libsession_util_nodejs';
import { from_hex } from 'libsodium-wrappers-sumo';
import { isArray, isEmpty, isEqual } from 'lodash';
@ -277,9 +277,12 @@ export function roomHasReactionsEnabled(openGroup?: OpenGroupV2Room) {
export function toFixedUint8ArrayOfLength<T extends number>(
data: Uint8Array,
length: T
): FixedSizeUint8Array<T> {
): Uint8ArrayFixedLength<T> {
if (data.length === length) {
return data as any as FixedSizeUint8Array<T>;
return {
buffer: data,
length,
};
}
throw new Error(
`toFixedUint8ArrayOfLength invalid. Expected length ${length} but got: ${data.length}`

@ -86,7 +86,7 @@ const sendMessageReaction = async (messageId: string, emoji: string) => {
return undefined;
}
let me = UserUtils.getOurPubKeyStrFromCache();
let me: string = UserUtils.getOurPubKeyStrFromCache();
let id = Number(found.get('sent_at'));
if (found.get('isPublic')) {

@ -1,22 +1,23 @@
/* eslint-disable import/extensions */
/* eslint-disable import/no-unresolved */
import { join } from 'path';
import {
GroupWrapperConstructor,
ContactInfoSet,
ContactsWrapperActionsCalls,
ConvoInfoVolatileWrapperActionsCalls,
GenericWrapperActionsCall,
GroupInfoSet,
GroupPubkeyType,
GroupWrapperConstructor,
LegacyGroupInfo,
MergeSingle,
MetaGroupWrapperActionsCalls,
ProfilePicture,
UserConfigWrapperActionsCalls,
UserGroupsWrapperActionsCalls,
UserGroupsSet,
MergeSingle,
UserGroupsWrapperActionsCalls,
} from 'libsession_util_nodejs';
// eslint-disable-next-line import/order
import { join } from 'path';
import { getAppRootPath } from '../../../node/getRootPath';
import { WorkerInterface } from '../../worker_interface';

Loading…
Cancel
Save