Merge pull request #73 from Bilb/feat-mark-03-group-expired

feat: mark 03 group expired if no hash retrieve at all
pull/3281/head
Audric Ackermann 2 months ago committed by GitHub
commit 3a0b6048f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -25,8 +25,8 @@ const StyledText = styled.span`
type NoticeBannerProps = {
text: string;
icon: SessionIconType;
onBannerClick: () => void;
icon?: SessionIconType;
onBannerClick?: () => void;
dataTestId: SessionDataTestId;
};
@ -41,12 +41,15 @@ export const NoticeBanner = (props: NoticeBannerProps) => {
alignItems={'center'}
data-testid={dataTestId}
onClick={event => {
if (!onBannerClick) {
return;
}
event?.preventDefault();
onBannerClick();
}}
>
<StyledText>{text}</StyledText>
<SessionIconButton iconType={icon} iconColor="inherit" iconSize="small" />
{icon ? <SessionIconButton iconType={icon} iconColor="inherit" iconSize="small" /> : null}
</StyledNoticeBanner>
);
};

@ -63,6 +63,7 @@ import { PubKey } from '../../session/types';
import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { localize } from '../../localization/localeTools';
import {
useConversationIsExpired03Group,
useSelectedConversationKey,
useSelectedIsPrivate,
useSelectedIsPublic,
@ -110,6 +111,20 @@ const ConvoLoadingSpinner = () => {
);
};
const GroupMarkedAsExpired = () => {
const selectedConvo = useSelectedConversationKey();
const isExpired03Group = useConversationIsExpired03Group(selectedConvo);
if (!selectedConvo || !PubKey.is03Pubkey(selectedConvo) || !isExpired03Group) {
return null;
}
return (
<NoticeBanner
text={window.i18n('groupNotUpdatedWarning')}
dataTestId="group-not-updated-30-days-banner"
/>
);
};
export class SessionConversation extends Component<Props, State> {
private readonly messageContainerRef: RefObject<HTMLDivElement>;
private dragCounter: number;
@ -259,6 +274,7 @@ export class SessionConversation extends Component<Props, State> {
<>
<div className="conversation-header">
<ConversationHeaderWithDetails />
<GroupMarkedAsExpired />
<OutdatedClientBanner
ourDisplayNameInProfile={ourDisplayNameInProfile}
selectedConversation={selectedConversation}

@ -32,6 +32,7 @@ import { closeRightPanel } from '../../../../state/ducks/conversations';
import { groupInfoActions } from '../../../../state/ducks/metaGroups';
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section';
import {
useConversationIsExpired03Group,
useSelectedConversationKey,
useSelectedDisplayNameInProfile,
useSelectedIsActive,
@ -235,6 +236,7 @@ const DeleteGroupPanelButton = () => {
const selectedUsername = useConversationUsername(convoId) || convoId;
const isPublic = useIsPublic(convoId);
const isGroupDestroyed = useIsGroupDestroyed(convoId);
const is03GroupExpired = useConversationIsExpired03Group(convoId);
const showItem = showDeleteGroupItem({
isGroup,
@ -242,6 +244,7 @@ const DeleteGroupPanelButton = () => {
isMessageRequestShown,
isPublic,
isGroupDestroyed,
is03GroupExpired,
});
if (!showItem || !convoId) {

@ -13,6 +13,7 @@ import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
import { showDeleteGroupItem } from './guard';
import { Localizer } from '../../../basic/Localizer';
import { useDisableLegacyGroupDeprecatedActions } from '../../../../hooks/useRefreshReleasedFeaturesTimestamp';
import { useConversationIsExpired03Group } from '../../../../state/selectors/selectedConversation';
export const DeleteGroupMenuItem = () => {
const convoId = useConvoIdFromContext();
@ -23,12 +24,15 @@ export const DeleteGroupMenuItem = () => {
const isPublic = useIsPublic(convoId);
const isGroupDestroyed = useIsGroupDestroyed(convoId);
const is03GroupExpired = useConversationIsExpired03Group(convoId);
const showLeave = showDeleteGroupItem({
isGroup,
isKickedFromGroup,
isMessageRequestShown,
isPublic,
isGroupDestroyed,
is03GroupExpired,
});
if (!showLeave) {

@ -48,6 +48,7 @@ export function showDeleteGroupItem(args: {
isMessageRequestShown: boolean;
isKickedFromGroup: boolean;
isGroupDestroyed: boolean;
is03GroupExpired: boolean;
}) {
return sharedEnabled(args) && !showLeaveGroupItem(args);
return (sharedEnabled(args) && !showLeaveGroupItem(args)) || args.is03GroupExpired;
}

@ -381,6 +381,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
toRet.hasOutdatedClient = this.getHasOutdatedClient();
}
if (this.getIsExpired03Group()) {
toRet.isExpired03Group = true;
}
if (
currentNotificationSetting &&
currentNotificationSetting !== ConversationNotificationSetting[0]
@ -2707,6 +2711,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return this.get('hasOutdatedClient');
}
public getIsExpired03Group() {
return PubKey.is03Pubkey(this.id) && !!this.get('isExpired03Group');
}
// #endregion
}

@ -101,6 +101,13 @@ export interface ConversationAttributes {
// TODO we need to make a migration to remove this value from the db since the implementation is hacky
/** to warn the user that the person he is talking to is using an old client which might cause issues */
hasOutdatedClient?: string;
/**
* An 03-group is expired if an admin didn't come online for the last 30 days.
* In that case, we might not have any keys on the swarm, and so restoring from seed would mean we can't actually
* send any messages/nor decrypt any.
*/
isExpired03Group?: boolean;
}
/**

@ -79,6 +79,7 @@ const allowedKeysFormatRowOfConversation = [
'priority',
'expirationMode',
'hasOutdatedClient',
'isExpired03Group',
];
export function formatRowOfConversation(
@ -213,6 +214,7 @@ const allowedKeysOfConversationAttributes = [
'priority',
'expirationMode',
'hasOutdatedClient',
'isExpired03Group',
];
/**

@ -109,6 +109,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion38,
updateToSessionSchemaVersion39,
updateToSessionSchemaVersion40,
updateToSessionSchemaVersion41,
];
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@ -2047,6 +2048,24 @@ function updateToSessionSchemaVersion40(currentVersion: number, db: BetterSqlite
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion41(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 41;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
// the 'isExpired03Group' field is used to keep track of an 03 group is expired
db.prepare(`ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isExpired03Group BOOLEAN;`).run();
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
export function printTableColumns(table: string, db: BetterSqlite3.Database) {
console.info(db.pragma(`table_info('${table}');`));
}

@ -474,6 +474,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
expirationMode,
expireTimer,
hasOutdatedClient,
isExpired03Group,
lastMessage,
lastMessageStatus,
lastMessageInteractionType,
@ -527,6 +528,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
expirationMode,
expireTimer,
hasOutdatedClient,
isExpired03Group,
lastMessageStatus,
lastMessage: shortenedLastMessage,
lastMessageInteractionType,

1
ts/react.d.ts vendored

@ -246,6 +246,7 @@ declare module 'react' {
| 'modal-heading'
| 'modal-description'
| 'error-message'
| 'group-not-updated-30-days-banner'
// modules profile name
| 'module-conversation__user__profile-name'
| 'module-message-search-result__header__name__profile-name'

@ -29,6 +29,7 @@ import {
WithTimestamp,
WithGetNow,
} from '../../types/with';
import { isDevProd } from '../../../shared/env_vars';
/**
* This is the base sub request class that every other type of request has to extend.
@ -636,6 +637,57 @@ export class DeleteAllFromGroupMsgNodeSubRequest extends DeleteAllSubRequest {
}
}
/**
* Delete all the normal and config messages from a group swarm.
* Note: only used for debugging purposes
*/
export class DeleteAllFromGroupNodeSubRequest extends DeleteAllSubRequest {
public readonly namespace = 'all';
public readonly adminSecretKey: Uint8Array;
public readonly destination: GroupPubkeyType;
constructor(args: WithGroupPubkey & WithSecretKey) {
super();
this.destination = args.groupPk;
this.adminSecretKey = args.secretKey;
if (isEmpty(this.adminSecretKey)) {
throw new Error('DeleteAllFromGroupMsgNodeSubRequest needs an adminSecretKey');
}
if (!isDevProd()) {
throw new Error('DeleteAllFromGroupNodeSubRequest can only be used on non-production build');
}
}
public async build() {
const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({
method: this.method,
namespace: this.namespace,
group: { authData: null, pubkeyHex: this.destination, secretKey: this.adminSecretKey },
});
if (!signDetails) {
throw new Error(
`[DeleteAllFromGroupMsgNodeSubRequest] SnodeSignature.getSnodeSignatureParamsUs returned an empty result`
);
}
return {
method: this.method,
params: {
...signDetails,
namespace: this.namespace,
},
};
}
public loggingId(): string {
return `${this.method}-${ed25519Str(this.destination)}-${this.namespace}`;
}
public getDestination() {
return this.destination;
}
}
export class DeleteHashesFromUserNodeSubRequest extends DeleteSubRequest {
public readonly messageHashes: Array<string>;
public readonly destination: PubkeyType;
@ -1336,7 +1388,8 @@ export type RawSnodeSubRequests =
| SubaccountRevokeSubRequest
| SubaccountUnrevokeSubRequest
| GetExpiriesFromNodeSubRequest
| DeleteAllFromGroupMsgNodeSubRequest;
| DeleteAllFromGroupMsgNodeSubRequest
| DeleteAllFromGroupNodeSubRequest;
export type BuiltSnodeSubRequests = AwaitedReturn<RawSnodeSubRequests['build']>;

@ -94,7 +94,7 @@ async function getGroupPromoteMessage({
type ParamsShared = {
groupPk: GroupPubkeyType;
namespace: SnodeNamespacesGroup;
namespace: SnodeNamespacesGroup | 'all';
method: 'retrieve' | 'store' | 'delete_all';
};
@ -150,7 +150,7 @@ export type GroupDetailsNeededForSignature = Pick<
type StoreOrRetrieve = { method: 'store' | 'retrieve'; namespace: SnodeNamespacesGroup };
type DeleteHashes = { method: 'delete'; hashes: Array<string> };
type DeleteAllNonConfigs = { method: 'delete_all'; namespace: SnodeNamespacesGroup };
type DeleteAllNonConfigs = { method: 'delete_all'; namespace: SnodeNamespacesGroup | 'all' };
async function getSnodeGroupSignature({
group,

@ -703,7 +703,22 @@ export class SwarmPolling {
);
return [];
}
const noConfigBeforeFetch = namespacesAndLastHashes.some(
m => !m.lastHash && SnodeNamespace.isGroupConfigNamespace(m.namespace)
);
const noConfigAfterFetch = namespacesAndLastHashesAfterFetch.some(
m => !m.lastHash && SnodeNamespace.isGroupConfigNamespace(m.namespace)
);
if (PubKey.is03Pubkey(pubkey) && noConfigBeforeFetch && noConfigAfterFetch) {
window.log.warn(`no configs before and after fetch of group: ${ed25519Str(pubkey)}`);
const convo = ConvoHub.use().get(pubkey);
if (convo && !convo.get('isExpired03Group')) {
convo.set({ isExpired03Group: true });
await convo.commit();
}
}
if (!results.length) {
return [];
}

@ -29,6 +29,7 @@ import {
StoreUserMessageSubRequest,
SubaccountRevokeSubRequest,
SubaccountUnrevokeSubRequest,
type DeleteAllFromGroupNodeSubRequest,
} from '../apis/snode_api/SnodeRequestTypes';
import { NotEmptyArrayOfBatchResults } from '../apis/snode_api/BatchResultEntry';
import { BatchRequests } from '../apis/snode_api/batchRequest';
@ -426,6 +427,7 @@ type SortedSubRequestsType<T extends PubkeyType | GroupPubkeyType> = Array<
| StoreRequestPerPubkey<T>
| DeleteHashesRequestPerPubkey<T>
| DeleteAllFromGroupMsgNodeSubRequest
| DeleteAllFromGroupNodeSubRequest
| SubaccountRevokeSubRequest
| SubaccountUnrevokeSubRequest
>;

@ -12,6 +12,7 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
DeleteAllFromGroupMsgNodeSubRequest,
DeleteAllFromGroupNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest,
MAX_SUBREQUESTS_COUNT,
StoreGroupKeysSubRequest,
@ -116,7 +117,9 @@ async function pushChangesToGroupSwarmIfNeeded({
WithRevokeSubRequest &
Partial<WithTimeoutMs> & {
supplementalKeysSubRequest?: StoreGroupKeysSubRequest;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest;
deleteAllMessagesSubRequest?:
| DeleteAllFromGroupMsgNodeSubRequest
| DeleteAllFromGroupNodeSubRequest;
extraStoreRequests: Array<StoreGroupMessageSubRequest>;
}): Promise<RunJobResult> {
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
@ -173,6 +176,7 @@ async function pushChangesToGroupSwarmIfNeeded({
m instanceof SubaccountRevokeSubRequest ||
m instanceof SubaccountUnrevokeSubRequest ||
m instanceof DeleteAllFromGroupMsgNodeSubRequest ||
m instanceof DeleteAllFromGroupNodeSubRequest ||
m instanceof DeleteHashesFromGroupNodeSubRequest
);

@ -212,6 +212,7 @@ export interface ReduxConversationType {
expirationMode?: DisappearingMessageConversationModeType;
expireTimer?: number;
hasOutdatedClient?: string;
isExpired03Group?: boolean;
isTyping?: boolean;
isBlocked?: boolean;
isKickedFromGroup?: boolean;

@ -436,3 +436,10 @@ export function useSelectedLastMessage() {
export function useSelectedMessageIds() {
return useSelector(getSelectedMessageIds);
}
export function useConversationIsExpired03Group(convoId?: string) {
return useSelector(
(state: StateType) =>
!!convoId && PubKey.is03Pubkey(convoId) && !!getSelectedConversation(state)?.isExpired03Group
);
}

Loading…
Cancel
Save