fix: allow non admin mods to delete message for deletion

pull/2407/head
Audric Ackermann 3 years ago
parent e8a1e07b68
commit e6cd277bd2

@ -29,6 +29,11 @@ window.sessionFeatureFlags = {
useTestNet: Boolean( useTestNet: Boolean(
process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('testnet') process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('testnet')
), ),
debug: {
debugFileServerRequests: false,
debugNonSnodeRequests: false,
debugOnionRequests: false,
},
}; };
window.versionInfo = { window.versionInfo = {

@ -61,7 +61,6 @@ export const MessageContextMenu = (props: Props) => {
isDeletable, isDeletable,
isDeletableForEveryone, isDeletableForEveryone,
isPublic, isPublic,
isOpenGroupV2,
weAreAdmin, weAreAdmin,
isSenderAdmin, isSenderAdmin,
text, text,
@ -208,9 +207,7 @@ export const MessageContextMenu = (props: Props) => {
</> </>
) : null} ) : null}
{weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null} {weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null}
{weAreAdmin && isOpenGroupV2 ? ( {weAreAdmin && isPublic ? <Item onClick={onUnban}>{window.i18n('unbanUser')}</Item> : null}
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
) : null}
{weAreAdmin && isPublic && !isSenderAdmin ? ( {weAreAdmin && isPublic && !isSenderAdmin ? (
<Item onClick={addModerator}>{window.i18n('addAsModerator')}</Item> <Item onClick={addModerator}>{window.i18n('addAsModerator')}</Item>
) : null} ) : null}

@ -124,6 +124,11 @@ export function useWeAreAdmin(convoId?: string) {
return Boolean(convoProps && convoProps.weAreAdmin); return Boolean(convoProps && convoProps.weAreAdmin);
} }
export function useWeAreModerator(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
return Boolean(convoProps && (convoProps.weAreAdmin || convoProps.weAreModerator));
}
export function useExpireTimer(convoId?: string) { export function useExpireTimer(convoId?: string) {
const convoProps = useConversationPropsById(convoId); const convoProps = useConversationPropsById(convoId);
return convoProps && convoProps.expireTimer; return convoProps && convoProps.expireTimer;

@ -245,8 +245,9 @@ const doDeleteSelectedMessagesInSOGS = async (
//#region open group v2 deletion //#region open group v2 deletion
// Get our Moderator status // Get our Moderator status
const isAdmin = conversation.isAdmin(ourDevicePubkey); const isAdmin = conversation.isAdmin(ourDevicePubkey);
const isModerator = conversation.isModerator(ourDevicePubkey);
if (!isAllOurs && !isAdmin) { if (!isAllOurs && !(isAdmin || isModerator)) {
ToastUtils.pushMessageDeleteForbidden(); ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds()); window.inboxStore?.dispatch(resetSelectedMessageIds());
return; return;

@ -1,5 +1,19 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import _, { isArray, isEmpty, isNumber, isString } from 'lodash'; import {
debounce,
defaults,
filter,
includes,
isArray,
isEmpty,
isEqual,
isNumber,
isString,
map,
sortBy,
throttle,
uniq,
} from 'lodash';
import { getMessageQueue } from '../session'; import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
@ -99,15 +113,15 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.initialPromise = Promise.resolve(); this.initialPromise = Promise.resolve();
autoBind(this); autoBind(this);
this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); this.throttledBumpTyping = throttle(this.bumpTyping, 300);
this.updateLastMessage = _.throttle(this.bouncyUpdateLastMessage.bind(this), 1000, { this.updateLastMessage = throttle(this.bouncyUpdateLastMessage.bind(this), 1000, {
trailing: true, trailing: true,
leading: true, leading: true,
}); });
this.throttledNotify = _.debounce(this.notify, 2000, { maxWait: 2000, trailing: true }); this.throttledNotify = debounce(this.notify, 2000, { maxWait: 2000, trailing: true });
//start right away the function is called, and wait 1sec before calling it again //start right away the function is called, and wait 1sec before calling it again
const markReadDebounced = _.debounce(this.markReadBouncy, 1000, { const markReadDebounced = debounce(this.markReadBouncy, 1000, {
leading: true, leading: true,
trailing: true, trailing: true,
}); });
@ -240,9 +254,22 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return groupAdmins && groupAdmins?.length > 0 ? groupAdmins : []; return groupAdmins && groupAdmins?.length > 0 ? groupAdmins : [];
} }
/**
* Get the list of moderators in that room, or an empty array
* Only to be called for opengroup conversations.
* This makes no sense for a private chat or an closed group, as closed group admins must be stored with getGroupAdmins
* @returns the list of moderators for the conversation if the conversation is public, or []
*/
public getGroupModerators(): Array<string> {
const groupModerators = this.get('groupModerators') as Array<string> | undefined;
return this.isPublic() && groupModerators && groupModerators?.length > 0 ? groupModerators : [];
}
// tslint:disable-next-line: cyclomatic-complexity max-func-body-length // tslint:disable-next-line: cyclomatic-complexity max-func-body-length
public getConversationModelProps(): ReduxConversationType { public getConversationModelProps(): ReduxConversationType {
const groupAdmins = this.getGroupAdmins(); const groupAdmins = this.getGroupAdmins();
const groupModerators = this.getGroupModerators();
// tslint:disable-next-line: cyclomatic-complexity // tslint:disable-next-line: cyclomatic-complexity
const isPublic = this.isPublic(); const isPublic = this.isPublic();
@ -253,6 +280,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const isPrivate = this.isPrivate(); const isPrivate = this.isPrivate();
const isGroup = !isPrivate; const isGroup = !isPrivate;
const weAreAdmin = this.isAdmin(ourNumber); const weAreAdmin = this.isAdmin(ourNumber);
const weAreModerator = this.isModerator(ourNumber); // only used for sogs
const isMe = this.isMe(); const isMe = this.isMe();
const isTyping = !!this.typingTimer; const isTyping = !!this.typingTimer;
const unreadCount = this.get('unreadCount') || undefined; const unreadCount = this.get('unreadCount') || undefined;
@ -290,6 +318,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
toRet.weAreAdmin = true; toRet.weAreAdmin = true;
} }
if (weAreModerator) {
toRet.weAreModerator = true;
}
if (isMe) { if (isMe) {
toRet.isMe = true; toRet.isMe = true;
} }
@ -343,21 +375,29 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (didApproveMe) { if (didApproveMe) {
toRet.didApproveMe = didApproveMe; toRet.didApproveMe = didApproveMe;
} }
if (isApproved) { if (isApproved) {
toRet.isApproved = isApproved; toRet.isApproved = isApproved;
} }
if (subscriberCount) { if (subscriberCount) {
toRet.subscriberCount = subscriberCount; toRet.subscriberCount = subscriberCount;
} }
if (groupAdmins && groupAdmins.length) { if (groupAdmins && groupAdmins.length) {
toRet.groupAdmins = _.uniq(groupAdmins); toRet.groupAdmins = uniq(groupAdmins);
} }
if (groupModerators && groupModerators.length) {
toRet.groupModerators = uniq(groupModerators);
}
if (members && members.length) { if (members && members.length) {
toRet.members = _.uniq(members); toRet.members = uniq(members);
} }
if (zombies && zombies.length) { if (zombies && zombies.length) {
toRet.zombies = _.uniq(zombies); toRet.zombies = uniq(zombies);
} }
if (expireTimer) { if (expireTimer) {
@ -384,10 +424,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
public async updateGroupAdmins(groupAdmins: Array<string>, shouldCommit: boolean) { public async updateGroupAdmins(groupAdmins: Array<string>, shouldCommit: boolean) {
const existingAdmins = _.uniq(_.sortBy(this.getGroupAdmins())); const existingAdmins = uniq(sortBy(this.getGroupAdmins()));
const newAdmins = _.uniq(_.sortBy(groupAdmins)); const newAdmins = uniq(sortBy(groupAdmins));
if (_.isEqual(existingAdmins, newAdmins)) { if (isEqual(existingAdmins, newAdmins)) {
return false; return false;
} }
this.set({ groupAdmins }); this.set({ groupAdmins });
@ -397,6 +437,23 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return true; return true;
} }
public async updateGroupModerators(groupModerators: Array<string>, shouldCommit: boolean) {
if (!this.isPublic()) {
throw new Error('group moderators are only possible on SOGS');
}
const existingModerators = uniq(sortBy(this.getGroupModerators()));
const newModerators = uniq(sortBy(groupModerators));
if (isEqual(existingModerators, newModerators)) {
return false;
}
this.set({ groupModerators: newModerators });
if (shouldCommit) {
await this.commit();
}
return true;
}
public async onReadMessage(message: MessageModel, readAt: number) { public async onReadMessage(message: MessageModel, readAt: number) {
// We mark as read everything older than this message - to clean up old stuff // We mark as read everything older than this message - to clean up old stuff
// still marked unread in the database. If the user generally doesn't read in // still marked unread in the database. If the user generally doesn't read in
@ -819,7 +876,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const messageModel = await this.addSingleOutgoingMessage({ const messageModel = await this.addSingleOutgoingMessage({
body, body,
quote: _.isEmpty(quote) ? undefined : quote, quote: isEmpty(quote) ? undefined : quote,
preview, preview,
attachments, attachments,
sent_at: networkTimestamp, sent_at: networkTimestamp,
@ -905,7 +962,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
let expireTimer = providedExpireTimer; let expireTimer = providedExpireTimer;
let source = providedSource; let source = providedSource;
_.defaults(options, { fromSync: false }); defaults(options, { fromSync: false });
if (!expireTimer) { if (!expireTimer) {
expireTimer = 0; expireTimer = 0;
@ -1088,7 +1145,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
const options = providedOptions || {}; const options = providedOptions || {};
_.defaults(options, { sendReadReceipts: true }); defaults(options, { sendReadReceipts: true });
const conversationId = this.id; const conversationId = this.id;
Notifications.clearByConversationID(conversationId); Notifications.clearByConversationID(conversationId);
@ -1125,7 +1182,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
window.inboxStore?.dispatch(conversationActions.messagesChanged(allProps)); window.inboxStore?.dispatch(conversationActions.messagesChanged(allProps));
} }
// Some messages we're marking read are local notifications with no sender // Some messages we're marking read are local notifications with no sender
read = _.filter(read, m => Boolean(m.sender)); read = filter(read, m => Boolean(m.sender));
const realUnreadCount = await this.getUnreadCount(); const realUnreadCount = await this.getUnreadCount();
if (read.length === 0) { if (read.length === 0) {
const cachedUnreadCountOnConvo = this.get('unreadCount'); const cachedUnreadCountOnConvo = this.get('unreadCount');
@ -1165,7 +1222,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
read = read.filter(item => !item.hasErrors); read = read.filter(item => !item.hasErrors);
if (read.length && options.sendReadReceipts) { if (read.length && options.sendReadReceipts) {
const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array<number>; const timestamps = map(read, 'timestamp').filter(t => !!t) as Array<number>;
await this.sendReadReceiptsIfNeeded(timestamps); await this.sendReadReceiptsIfNeeded(timestamps);
} }
} }
@ -1233,7 +1290,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// if you change this behavior, double check all setSessionProfile calls (especially the one in EditProfileDialog) // if you change this behavior, double check all setSessionProfile calls (especially the one in EditProfileDialog)
if (newProfile.avatarPath) { if (newProfile.avatarPath) {
const originalAvatar = this.get('avatarInProfile'); const originalAvatar = this.get('avatarInProfile');
if (!_.isEqual(originalAvatar, newProfile.avatarPath)) { if (!isEqual(originalAvatar, newProfile.avatarPath)) {
this.set({ avatarInProfile: newProfile.avatarPath }); this.set({ avatarInProfile: newProfile.avatarPath });
changes = true; changes = true;
} }
@ -1306,6 +1363,22 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey);
} }
/**
* Check if the provided pubkey is a moderator.
* Being a moderator only makes sense for a sogs as closed groups have their admin under the groupAdmins property
*/
public isModerator(pubKey?: string) {
if (!pubKey) {
throw new Error('isModerator() pubKey is falsy');
}
if (!this.isPublic()) {
return false;
}
const groupModerators = this.getGroupModerators();
return Array.isArray(groupModerators) && groupModerators.includes(pubKey);
}
public async setIsPinned(value: boolean) { public async setIsPinned(value: boolean) {
if (value !== this.isPinned()) { if (value !== this.isPinned()) {
this.set({ this.set({
@ -1363,6 +1436,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
* Saves the infos of that room directly on the conversation table. * Saves the infos of that room directly on the conversation table.
* This does not write anything to the db if no changes are detected * This does not write anything to the db if no changes are detected
*/ */
// tslint:disable-next-line: cyclomatic-complexity
public async setPollInfo(infos?: { public async setPollInfo(infos?: {
subscriberCount: number; subscriberCount: number;
read: boolean; read: boolean;
@ -1371,6 +1445,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
details: { details: {
admins?: Array<string>; admins?: Array<string>;
image_id?: number; image_id?: number;
moderators?: Array<string>;
}; };
}) { }) {
if (!infos || isEmpty(infos)) { if (!infos || isEmpty(infos)) {
@ -1403,24 +1478,26 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
if (details.admins && isArray(details.admins)) { if (details.admins && isArray(details.admins)) {
const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id); const replacedWithOurRealSessionId = await this.replaceWithOurRealSessionId(details.admins);
const ourBlindedPubkeyForThisSogs =
roomInfos && roomHasBlindEnabled(roomInfos)
? await findCachedOurBlindedPubkeyOrLookItUp(
roomInfos?.serverPublicKey,
await getSodiumRenderer()
)
: UserUtils.getOurPubKeyStrFromCache();
const replacedWithOurRealSessionId = details.admins.map(m =>
m === ourBlindedPubkeyForThisSogs ? UserUtils.getOurPubKeyStrFromCache() : m
);
const adminChanged = await this.updateGroupAdmins(replacedWithOurRealSessionId, false); const adminChanged = await this.updateGroupAdmins(replacedWithOurRealSessionId, false);
if (adminChanged) { if (adminChanged) {
hasChange = adminChanged; hasChange = adminChanged;
} }
} }
if (details.moderators && isArray(details.moderators)) {
const replacedWithOurRealSessionId = await this.replaceWithOurRealSessionId(
details.moderators
);
const moderatorsChanged = await this.updateGroupModerators(
replacedWithOurRealSessionId,
false
);
if (moderatorsChanged) {
hasChange = moderatorsChanged;
}
}
if (this.isOpenGroupV2() && details.image_id && isNumber(details.image_id)) { if (this.isOpenGroupV2() && details.image_id && isNumber(details.image_id)) {
const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id); const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id);
if (roomInfos) { if (roomInfos) {
@ -1458,7 +1535,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
public hasMember(pubkey: string) { public hasMember(pubkey: string) {
return _.includes(this.get('members'), pubkey); return includes(this.get('members'), pubkey);
} }
// returns true if this is a closed/medium or open group // returns true if this is a closed/medium or open group
public isGroup() { public isGroup() {
@ -1836,9 +1913,22 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
.sendToPubKey(device, typingMessage) .sendToPubKey(device, typingMessage)
.catch(window?.log?.error); .catch(window?.log?.error);
} }
private async replaceWithOurRealSessionId(toReplace: Array<string>) {
const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id);
const sodium = await getSodiumRenderer();
const ourBlindedPubkeyForThisSogs =
roomInfos && roomHasBlindEnabled(roomInfos)
? await findCachedOurBlindedPubkeyOrLookItUp(roomInfos?.serverPublicKey, sodium)
: UserUtils.getOurPubKeyStrFromCache();
const replacedWithOurRealSessionId = toReplace.map(m =>
m === ourBlindedPubkeyForThisSogs ? UserUtils.getOurPubKeyStrFromCache() : m
);
return replacedWithOurRealSessionId;
}
} }
const throttledAllConversationsDispatch = _.debounce( const throttledAllConversationsDispatch = debounce(
() => { () => {
if (updatesToDispatch.size === 0) { if (updatesToDispatch.size === 0) {
return; return;

@ -42,7 +42,8 @@ export interface ConversationAttributes {
*/ */
lastMessage: string | null; lastMessage: string | null;
lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group
groupAdmins: Array<string>; groupAdmins: Array<string>; // for sogs and closed group: the admins of that group.
groupModerators: Array<string>; // for sogs only, this is the moderator in that room.
isKickedFromGroup: boolean; isKickedFromGroup: boolean;
subscriberCount: number; subscriberCount: number;

@ -28,6 +28,7 @@ export function toSqliteBoolean(val: boolean): number {
// this is used to make sure when storing something in the database you remember to add the wrapping for it in formatRowOfConversation // this is used to make sure when storing something in the database you remember to add the wrapping for it in formatRowOfConversation
const allowedKeysFormatRowOfConversation = [ const allowedKeysFormatRowOfConversation = [
'groupAdmins', 'groupAdmins',
'groupModerators',
'members', 'members',
'zombies', 'zombies',
'isTrustedForAttachmentDownload', 'isTrustedForAttachmentDownload',
@ -80,7 +81,7 @@ export function formatRowOfConversation(row?: Record<string, any>): Conversation
const convo: ConversationAttributes = omit(row, 'json') as ConversationAttributes; const convo: ConversationAttributes = omit(row, 'json') as ConversationAttributes;
// if the stringified array of admins/members/zombies length is less than 5, // if the stringified array of admins/moderators/members/zombies length is less than 5,
// we consider there is nothing to parse and just return [] // we consider there is nothing to parse and just return []
const minLengthNoParsing = 5; const minLengthNoParsing = 5;
@ -88,6 +89,11 @@ export function formatRowOfConversation(row?: Record<string, any>): Conversation
row.groupAdmins?.length && row.groupAdmins.length > minLengthNoParsing row.groupAdmins?.length && row.groupAdmins.length > minLengthNoParsing
? jsonToArray(row.groupAdmins) ? jsonToArray(row.groupAdmins)
: []; : [];
convo.groupModerators =
row.groupModerators?.length && row.groupModerators.length > minLengthNoParsing
? jsonToArray(row.groupModerators)
: [];
convo.members = convo.members =
row.members?.length && row.members.length > minLengthNoParsing ? jsonToArray(row.members) : []; row.members?.length && row.members.length > minLengthNoParsing ? jsonToArray(row.members) : [];
convo.zombies = convo.zombies =
@ -147,6 +153,7 @@ export function formatRowOfConversation(row?: Record<string, any>): Conversation
const allowedKeysOfConversationAttributes = [ const allowedKeysOfConversationAttributes = [
'groupAdmins', 'groupAdmins',
'groupModerators',
'members', 'members',
'zombies', 'zombies',
'isTrustedForAttachmentDownload', 'isTrustedForAttachmentDownload',

@ -773,6 +773,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToLokiSchemaVersion23, updateToLokiSchemaVersion23,
updateToLokiSchemaVersion24, updateToLokiSchemaVersion24,
updateToLokiSchemaVersion25, updateToLokiSchemaVersion25,
updateToLokiSchemaVersion26,
]; ];
function updateToLokiSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) { function updateToLokiSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@ -1588,6 +1589,24 @@ function updateToLokiSchemaVersion25(currentVersion: number, db: BetterSqlite3.D
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
} }
function updateToLokiSchemaVersion26(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 26;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN groupModerators TEXT DEFAULT "[]"; -- those are for sogs only (for closed groups we only need the groupAdmins)
`);
writeLokiSchemaVersion(targetVersion, db);
})();
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
// function printTableColumns(table: string, db: BetterSqlite3.Database) { // function printTableColumns(table: string, db: BetterSqlite3.Database) {
// console.warn(db.pragma(`table_info('${table}');`)); // console.warn(db.pragma(`table_info('${table}');`));
// } // }
@ -1999,6 +2018,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3
lastMessage, lastMessage,
lastJoinedTimestamp, lastJoinedTimestamp,
groupAdmins, groupAdmins,
groupModerators,
isKickedFromGroup, isKickedFromGroup,
subscriberCount, subscriberCount,
readCapability, readCapability,
@ -2039,6 +2059,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3
lastMessage, lastMessage,
lastJoinedTimestamp, lastJoinedTimestamp,
groupAdmins, groupAdmins,
groupModerators,
isKickedFromGroup, isKickedFromGroup,
subscriberCount, subscriberCount,
readCapability, readCapability,
@ -2071,6 +2092,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3
$lastMessage, $lastMessage,
$lastJoinedTimestamp, $lastJoinedTimestamp,
$groupAdmins, $groupAdmins,
$groupModerators,
$isKickedFromGroup, $isKickedFromGroup,
$subscriberCount, $subscriberCount,
$readCapability, $readCapability,
@ -2106,6 +2128,8 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3
lastJoinedTimestamp, lastJoinedTimestamp,
groupAdmins: groupAdmins && groupAdmins.length ? arrayStrToJson(groupAdmins) : '[]', groupAdmins: groupAdmins && groupAdmins.length ? arrayStrToJson(groupAdmins) : '[]',
groupModerators:
groupModerators && groupModerators.length ? arrayStrToJson(groupModerators) : '[]',
isKickedFromGroup: toSqliteBoolean(isKickedFromGroup), isKickedFromGroup: toSqliteBoolean(isKickedFromGroup),
subscriberCount, subscriberCount,
readCapability: toSqliteBoolean(readCapability), readCapability: toSqliteBoolean(readCapability),
@ -3789,6 +3813,7 @@ function fillWithTestData(numConvosToAdd: number, numMsgsToAdd: number) {
didApproveMe: false, didApproveMe: false,
expireTimer: 0, expireTimer: 0,
groupAdmins: [], groupAdmins: [],
groupModerators: [],
isApproved: false, isApproved: false,
isKickedFromGroup: false, isKickedFromGroup: false,
isPinned: false, isPinned: false,

@ -73,12 +73,18 @@ export const downloadFileFromFileServer = async (
return null; return null;
} }
const urlToGet = `${POST_GET_FILE_ENDPOINT}/${fileId}`;
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`about to try to download fsv2: "${urlToGet}"`);
}
const result = await OnionSending.getBinaryViaOnionV4FromFileServer({ const result = await OnionSending.getBinaryViaOnionV4FromFileServer({
abortSignal: new AbortController().signal, abortSignal: new AbortController().signal,
endpoint: `${POST_GET_FILE_ENDPOINT}/${fileId}`, endpoint: urlToGet,
method: 'GET', method: 'GET',
}); });
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`download fsv2: "${urlToGet} got result:`, JSON.stringify(result));
}
if (!result) { if (!result) {
return null; return null;
} }

@ -12,6 +12,7 @@ import {
OpenGroupBatchRow, OpenGroupBatchRow,
parseBatchGlobalStatusCode, parseBatchGlobalStatusCode,
sogsBatchSend, sogsBatchSend,
SubRequestMessagesObjectType,
} from '../sogsv3/sogsV3BatchPoll'; } from '../sogsv3/sogsV3BatchPoll';
import { handleBatchPollResults } from '../sogsv3/sogsApiV3'; import { handleBatchPollResults } from '../sogsv3/sogsApiV3';
import { import {
@ -325,7 +326,8 @@ export class OpenGroupServerPoller {
export const getRoomAndUpdateLastFetchTimestamp = async ( export const getRoomAndUpdateLastFetchTimestamp = async (
conversationId: string, conversationId: string,
newMessages: Array<OpenGroupMessageV2 | OpenGroupMessageV4> newMessages: Array<OpenGroupMessageV2 | OpenGroupMessageV4>,
subRequest: SubRequestMessagesObjectType
) => { ) => {
const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversationId); const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversationId);
if (!roomInfos || !roomInfos.serverUrl || !roomInfos.roomId) { if (!roomInfos || !roomInfos.serverUrl || !roomInfos.roomId) {
@ -336,7 +338,7 @@ export const getRoomAndUpdateLastFetchTimestamp = async (
// if we got no new messages, just write our last update timestamp to the db // if we got no new messages, just write our last update timestamp to the db
roomInfos.lastFetchTimestamp = Date.now(); roomInfos.lastFetchTimestamp = Date.now();
window?.log?.info( window?.log?.info(
`No new messages for ${roomInfos.roomId}... just updating our last fetched timestamp` `No new messages for ${subRequest?.roomId}:${subRequest?.sinceSeqNo}... just updating our last fetched timestamp`
); );
await OpenGroupData.saveV2OpenGroupRoom(roomInfos); await OpenGroupData.saveV2OpenGroupRoom(roomInfos);
return null; return null;

@ -73,13 +73,13 @@ async function handlePollInfoResponse(
token: string; token: string;
upload: boolean; upload: boolean;
write: boolean; write: boolean;
details: { admins?: Array<string>; image_id: number }; details: { admins?: Array<string>; image_id: number; moderators?: Array<string> };
}, },
serverUrl: string, serverUrl: string,
roomIdsStillPolled: Set<string> roomIdsStillPolled: Set<string>
) { ) {
if (statusCode !== 200) { if (statusCode !== 200) {
window.log.info('handlePollInfoResponse subRequest status code is not 200'); window.log.info('handlePollInfoResponse subRequest status code is not 200:', statusCode);
return; return;
} }
@ -109,7 +109,7 @@ async function handlePollInfoResponse(
write, write,
upload, upload,
subscriberCount: active_users, subscriberCount: active_users,
details: pick(details, 'admins', 'image_id'), details: pick(details, 'admins', 'image_id', 'moderators'),
}); });
} }
@ -190,7 +190,11 @@ const handleMessagesResponseV4 = async (
} }
const convoId = getOpenGroupV2ConversationId(serverUrl, roomId); const convoId = getOpenGroupV2ConversationId(serverUrl, roomId);
const roomInfos = await getRoomAndUpdateLastFetchTimestamp(convoId, messages); const roomInfos = await getRoomAndUpdateLastFetchTimestamp(
convoId,
messages,
subrequestOption.messages
);
if (!roomInfos || !roomInfos.conversationId) { if (!roomInfos || !roomInfos.conversationId) {
return; return;
} }

@ -121,12 +121,16 @@ export type SubrequestOptionType = 'capabilities' | 'messages' | 'pollInfo' | 'i
export type SubRequestCapabilitiesType = { type: 'capabilities' }; export type SubRequestCapabilitiesType = { type: 'capabilities' };
export type SubRequestMessagesObjectType =
| {
roomId: string;
sinceSeqNo?: number;
}
| undefined;
export type SubRequestMessagesType = { export type SubRequestMessagesType = {
type: 'messages'; type: 'messages';
messages?: { messages?: SubRequestMessagesObjectType;
roomId: string;
sinceSeqNo?: number;
};
}; };
export type SubRequestPollInfoType = { export type SubRequestPollInfoType = {

@ -782,6 +782,16 @@ async function sendOnionRequestHandlingSnodeEject({
abortSignal, abortSignal,
useV4, useV4,
}); });
if (window.sessionFeatureFlags?.debug.debugOnionRequests) {
window.log.info(
`sendOnionRequestHandlingSnodeEject: sendOnionRequestNoRetries: useV4:${useV4} destSnodeX25519:${destSnodeX25519}; \nfinalDestOptions:${JSON.stringify(
finalDestOptions
)}; \nfinalRelayOptions:${JSON.stringify(finalRelayOptions)}\n\n result: ${JSON.stringify(
result
)}`
);
}
response = result.response; response = result.response;
if ( if (
!isEmpty(finalRelayOptions) && !isEmpty(finalRelayOptions) &&

@ -102,6 +102,13 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
} }
const payloadObj = buildSendViaOnionPayload(url, fetchOptions); const payloadObj = buildSendViaOnionPayload(url, fetchOptions);
if (window.sessionFeatureFlags?.debug.debugNonSnodeRequests) {
window.log.info(
'sendViaOnionV4ToNonSnodeWithRetries: buildSendViaOnionPayload returned ',
JSON.stringify(payloadObj)
);
}
// if protocol is forced to 'http:' => just use http (without the ':'). // if protocol is forced to 'http:' => just use http (without the ':').
// otherwise use https as protocol (this is the default) // otherwise use https as protocol (this is the default)
const forcedHttp = url.protocol === PROTOCOLS.HTTP; const forcedHttp = url.protocol === PROTOCOLS.HTTP;
@ -121,7 +128,12 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
result = await pRetry( result = await pRetry(
async () => { async () => {
const pathNodes = await OnionSending.getOnionPathForSending(); const pathNodes = await OnionSending.getOnionPathForSending();
if (window.sessionFeatureFlags?.debug.debugNonSnodeRequests) {
window.log.info(
'sendViaOnionV4ToNonSnodeWithRetries: getOnionPathForSending returned',
JSON.stringify(pathNodes)
);
}
if (!pathNodes) { if (!pathNodes) {
throw new Error('getOnionPathForSending is emtpy'); throw new Error('getOnionPathForSending is emtpy');
} }
@ -140,6 +152,12 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
useV4: true, useV4: true,
throwErrors, throwErrors,
}); });
if (window.sessionFeatureFlags?.debug.debugNonSnodeRequests) {
window.log.info(
'sendViaOnionV4ToNonSnodeWithRetries: sendOnionRequestHandlingSnodeEject returned: ',
JSON.stringify(onionV4Response)
);
}
if (abortSignal?.aborted) { if (abortSignal?.aborted) {
// if the request was aborted, we just want to stop retries. // if the request was aborted, we just want to stop retries.
@ -175,6 +193,12 @@ const sendViaOnionV4ToNonSnodeWithRetries = async (
bodyBinary: decodedV4?.bodyBinary || null, bodyBinary: decodedV4?.bodyBinary || null,
}; };
} }
if (foundStatusCode === 404) {
// this is most likely that a 404 won't fix itself. So just stop right here retries by throwing a non retryable error
throw new pRetry.AbortError(
`Got 404 while sendViaOnionV4ToNonSnodeWithRetries with url:${url}. Stopping retries`
);
}
// we consider those cases as an error, and trigger a retry (if possible), by throwing a non-abortable error // we consider those cases as an error, and trigger a retry (if possible), by throwing a non-abortable error
throw new Error( throw new Error(
`sendViaOnionV4ToNonSnodeWithRetries failed with status code: ${foundStatusCode}. Retrying...` `sendViaOnionV4ToNonSnodeWithRetries failed with status code: ${foundStatusCode}. Retrying...`
@ -419,6 +443,9 @@ async function getBinaryViaOnionV4FromFileServer(sendOptions: {
} }
const builtUrl = new URL(`${fileServerURL}${endpoint}`); const builtUrl = new URL(`${fileServerURL}${endpoint}`);
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(`getBinaryViaOnionV4FromFileServer fsv2: "${builtUrl} `);
}
const res = await OnionSending.sendViaOnionV4ToNonSnodeWithRetries( const res = await OnionSending.sendViaOnionV4ToNonSnodeWithRetries(
fileServerPubKey, fileServerPubKey,
builtUrl, builtUrl,
@ -432,6 +459,12 @@ async function getBinaryViaOnionV4FromFileServer(sendOptions: {
abortSignal abortSignal
); );
if (window.sessionFeatureFlags?.debug.debugFileServerRequests) {
window.log.info(
`getBinaryViaOnionV4FromFileServer fsv2: "${builtUrl}; got:`,
JSON.stringify(res)
);
}
return res as OnionV4BinarySnodeResponse; return res as OnionV4BinarySnodeResponse;
} }

@ -238,6 +238,7 @@ export interface ReduxConversationType {
isGroup?: boolean; isGroup?: boolean;
isPrivate?: boolean; isPrivate?: boolean;
weAreAdmin?: boolean; weAreAdmin?: boolean;
weAreModerator?: boolean;
unreadCount?: number; unreadCount?: number;
mentionedUs?: boolean; mentionedUs?: boolean;
isSelected?: boolean; isSelected?: boolean;
@ -249,7 +250,8 @@ export interface ReduxConversationType {
subscriberCount?: number; subscriberCount?: number;
left?: boolean; left?: boolean;
avatarPath?: string | null; // absolute filepath to the avatar avatarPath?: string | null; // absolute filepath to the avatar
groupAdmins?: Array<string>; // admins for closed groups and moderators for open groups groupAdmins?: Array<string>; // admins for closed groups and admins for open groups
groupModerators?: Array<string>; // only for opengroups: moderators
members?: Array<string>; // members for closed groups only members?: Array<string>; // members for closed groups only
zombies?: Array<string>; // members for closed groups only zombies?: Array<string>; // members for closed groups only

@ -826,16 +826,21 @@ export const getMessagePropsByMessageId = createSelector(
const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || []; const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false; const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
const groupModerators = (isGroup && foundMessageConversation.groupModerators) || [];
const weAreModerator = groupModerators.includes(ourPubkey) || false;
// A message is deletable if // A message is deletable if
// either we sent it, // either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us) // or the convo is not a public one (in this case, we will only be able to delete for us)
// or the convo is public and we are an admin // or the convo is public and we are an admin or moderator
const isDeletable = sender === ourPubkey || !isPublic || (isPublic && !!weAreAdmin); const isDeletable =
sender === ourPubkey || !isPublic || (isPublic && (weAreAdmin || weAreModerator));
// A message is deletable for everyone if // A message is deletable for everyone if
// either we sent it no matter what the conversation type, // either we sent it no matter what the conversation type,
// or the convo is public and we are an admin // or the convo is public and we are an admin or moderator
const isDeletableForEveryone = sender === ourPubkey || (isPublic && !!weAreAdmin) || false; const isDeletableForEveryone =
sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false;
const isSenderAdmin = groupAdmins.includes(sender); const isSenderAdmin = groupAdmins.includes(sender);
const senderIsUs = sender === ourPubkey; const senderIsUs = sender === ourPubkey;

@ -36,6 +36,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
expireTimer: 0, expireTimer: 0,
@ -64,6 +65,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
expireTimer: 0, expireTimer: 0,
@ -92,6 +94,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
expireTimer: 0, expireTimer: 0,
@ -120,6 +123,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
expireTimer: 0, expireTimer: 0,
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
@ -149,6 +153,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: false, isPinned: false,
@ -192,6 +197,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: false, isPinned: false,
@ -221,6 +227,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: false, isPinned: false,
@ -250,6 +257,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: true, isPinned: true,
@ -278,6 +286,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: true, isPinned: true,
@ -307,6 +316,7 @@ describe('state/selectors/conversations', () => {
avatarPath: '', avatarPath: '',
groupAdmins: [], groupAdmins: [],
groupModerators: [],
lastMessage: undefined, lastMessage: undefined,
members: [], members: [],
isPinned: false, isPinned: false,

5
ts/window.d.ts vendored

@ -38,6 +38,11 @@ declare global {
sessionFeatureFlags: { sessionFeatureFlags: {
useOnionRequests: boolean; useOnionRequests: boolean;
useTestNet: boolean; useTestNet: boolean;
debug: {
debugFileServerRequests: boolean;
debugNonSnodeRequests: boolean;
debugOnionRequests: boolean;
};
}; };
SessionSnodeAPI: SessionSnodeAPI; SessionSnodeAPI: SessionSnodeAPI;
onLogin: any; onLogin: any;

Loading…
Cancel
Save