Skip to content

Commit 1cb680e

Browse files
committed
chore: added profileUpdatedAt sql column and convo attrs
1 parent cf51c69 commit 1cb680e

File tree

9 files changed

+130
-19
lines changed

9 files changed

+130
-19
lines changed

ts/data/data.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
FindAllMessageHashesInConversationTypeArgs,
3636
} from './sharedDataTypes';
3737
import { GuardNode, Snode } from './types';
38-
import { makeMessageModels } from '../models/models';
3938

4039
const ERASE_SQL_KEY = 'erase-sql-key';
4140
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
@@ -324,6 +323,10 @@ async function filterAlreadyFetchedOpengroupMessage(
324323
return msgDetailsNotAlreadyThere || [];
325324
}
326325

326+
function makeMessageModels(modelsOrAttrs: Array<MessageAttributes>) {
327+
return modelsOrAttrs.map(a => new MessageModel(a));
328+
}
329+
327330
/**
328331
* Fetch all messages that match the sender pubkey and sent_at timestamp
329332
* @param propsList An array of objects containing a source (the sender id) and timestamp of the message - not to be confused with the serverTimestamp. This is equivalent to sent_at

ts/models/conversation.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ import type {
149149
ConversationInteractionType,
150150
} from '../interactions/types';
151151
import type { LastMessageStatusType } from '../state/ducks/types';
152+
import { OutgoingUserProfile } from '../types/message';
152153

153154
type InMemoryConvoInfos = {
154155
mentionedUs: boolean;
@@ -1596,14 +1597,59 @@ export class ConversationModel extends Model<ConversationAttributes> {
15961597
return this.isPrivate() ? this.get('nickname') || undefined : undefined;
15971598
}
15981599

1600+
/**
1601+
* Returns the profile key attributes of this instance.
1602+
* If the attribute is unset, empty, or not a string, returns `undefined`.
1603+
*/
15991604
public getProfileKey(): string | undefined {
1600-
return this.get('profileKey');
1605+
const profileKey = this.get('profileKey');
1606+
if (!profileKey || !isString(profileKey)) {
1607+
return undefined;
1608+
}
1609+
return profileKey;
16011610
}
16021611

16031612
public getAvatarPointer(): string | undefined {
16041613
return this.get('avatarPointer');
16051614
}
16061615

1616+
public getProfileUpdatedSeconds() {
1617+
if (!this.isPrivate()) {
1618+
throw new Error('Cannot call profileUpdatedSeconds for a non private conversation');
1619+
}
1620+
const profileUpdatedSeconds = this.get('profileUpdatedSeconds');
1621+
if (
1622+
!isNumber(profileUpdatedSeconds) ||
1623+
!isFinite(profileUpdatedSeconds) ||
1624+
profileUpdatedSeconds < 0
1625+
) {
1626+
return 0;
1627+
}
1628+
1629+
return profileUpdatedSeconds;
1630+
}
1631+
1632+
/**
1633+
* A private profile can have name and profileUpdatedSeconds unset,
1634+
* but it must have both profilePicture and profileKey set for it to be returned
1635+
*/
1636+
public getPrivateProfileDetails(): OutgoingUserProfile {
1637+
if (!this.isPrivate()) {
1638+
throw new Error('getPrivateProfileDetails can only be called on private conversations');
1639+
}
1640+
const avatarPointer = this.getAvatarPointer() ?? null;
1641+
const displayName = this.getRealSessionUsername() ?? '';
1642+
const profileKey = this.getProfileKey() ?? null;
1643+
const updatedAtSeconds = this.getProfileUpdatedSeconds();
1644+
1645+
return new OutgoingUserProfile({
1646+
avatarPointer,
1647+
displayName,
1648+
profileKey,
1649+
updatedAtSeconds,
1650+
});
1651+
}
1652+
16071653
public setAvatarPointer(avatarPointer: string | undefined) {
16081654
if (avatarPointer === this.getAvatarPointer()) {
16091655
return;

ts/models/conversationAttributes.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,37 @@ export interface ConversationAttributes {
9292

9393
displayNameInProfile?: string; // no matter the type of conversation, this is the real name as set by the user/name of the open or closed group
9494
nickname?: string; // this is the name WE gave to that user (only applicable to private chats, not closed group neither opengroups)
95-
profileKey?: string; // Consider this being a hex string if it is set
96-
triggerNotificationsFor: ConversationNotificationSettingType;
95+
profileKey?: string; // If set, this is a hex string.
9796

9897
/**
9998
* This is the url of the avatar on the file server v2 or sogs server.
10099
* We use this to detect if we need to re-download the avatar from someone/ a community.
101100
*/
102101
avatarPointer?: string;
102+
/**
103+
* This is the timestamp of the last time the profile of that user was updated.
104+
* Only used for private chats (or blinded), but not for groups avatars nor communities.
105+
* An incoming avatarPointer & profileKey will only be applied if the provided
106+
* profileUpdatedSeconds is more recent than the currently stored one.
107+
*/
108+
profileUpdatedSeconds?: number;
109+
triggerNotificationsFor: ConversationNotificationSettingType;
103110
/** in seconds, 0 means no expiration */
104111
expireTimer: number;
105112

106-
members: Array<string>; // groups only members are all members for this group (not used for communities)
107-
groupAdmins: Array<string>; // for sogs and closed group: the unique admins of that group
113+
/**
114+
* Members of 03-groups and legacy groups until we remove them entirely (not used for communities)
115+
*/
116+
members: Array<string>;
117+
/**
118+
* For sogs and closed group: the unique admins of that group
119+
*/
120+
groupAdmins: Array<string>;
108121

109-
priority: number; // -1 = hidden (contact and NTS only), 0 = normal, 1 = pinned
122+
/**
123+
* -1 = hidden (contact and NTS only), 0 = normal, 1 = pinned
124+
*/
125+
priority: number;
110126

111127
isApproved: boolean; // if we sent a message request or sent a message to this contact, we approve them. If isApproved & didApproveMe, a message request becomes a contact
112128
didApproveMe: boolean; // if our message request was approved already (or they've sent us a message request/message themselves). If isApproved & didApproveMe, a message request becomes a contact

ts/models/models.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { assign, cloneDeep } from 'lodash';
2-
import { MessageModel } from './message';
3-
import type { MessageAttributes } from './messageType';
42

53
export type ModelAttributes = Record<string, any> & { id: string };
64

@@ -45,7 +43,3 @@ export abstract class Model<T extends ModelAttributes> {
4543
return cloneDeep(this._attributes);
4644
}
4745
}
48-
49-
export function makeMessageModels(modelsOrAttrs: Array<MessageAttributes>) {
50-
return modelsOrAttrs.map(a => new MessageModel(a));
51-
}

ts/node/database_utility.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as BetterSqlite3 from '@signalapp/better-sqlite3';
2-
import { difference, isNumber, omit, pick } from 'lodash';
2+
import { difference, isFinite, isNumber, omit, pick } from 'lodash';
33
import {
44
ConversationAttributes,
55
ConversationAttributesWithNotSavedOnes,
@@ -59,6 +59,7 @@ const allowedKeysFormatRowOfConversation = [
5959
'lastMessageInteractionType',
6060
'lastMessageInteractionStatus',
6161
'triggerNotificationsFor',
62+
'profileUpdatedSeconds',
6263
'unreadCount',
6364
'lastJoinedTimestamp',
6465
'expireTimer',
@@ -139,10 +140,14 @@ export function formatRowOfConversation(
139140
convo.lastMessageStatus = undefined;
140141
}
141142

142-
if (!isNumber(convo.blocksSogsMsgReqsTimestamp)) {
143+
if (!isNumber(convo.blocksSogsMsgReqsTimestamp) || !isFinite(convo.blocksSogsMsgReqsTimestamp)) {
143144
convo.blocksSogsMsgReqsTimestamp = 0;
144145
}
145146

147+
if (!isNumber(convo.profileUpdatedSeconds) || !isFinite(convo.profileUpdatedSeconds)) {
148+
convo.profileUpdatedSeconds = 0;
149+
}
150+
146151
if (!convo.lastMessageInteractionType) {
147152
convo.lastMessageInteractionType = null;
148153
}
@@ -189,6 +194,7 @@ const allowedKeysOfConversationAttributes = [
189194
'lastMessageInteractionType',
190195
'lastMessageInteractionStatus',
191196
'triggerNotificationsFor',
197+
'profileUpdatedSeconds',
192198
'lastJoinedTimestamp',
193199
'expireTimer',
194200
'active_at',

ts/node/migration/sessionMigrations.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const LOKI_SCHEMA_VERSIONS: Array<
120120
updateToSessionSchemaVersion45,
121121
updateToSessionSchemaVersion46,
122122
updateToSessionSchemaVersion47,
123+
updateToSessionSchemaVersion48,
123124
];
124125

125126
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
@@ -2181,6 +2182,24 @@ async function updateToSessionSchemaVersion47(currentVersion: number, db: Better
21812182
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
21822183
}
21832184

2185+
async function updateToSessionSchemaVersion48(currentVersion: number, db: BetterSqlite3.Database) {
2186+
const targetVersion = 48;
2187+
if (currentVersion >= targetVersion) {
2188+
return;
2189+
}
2190+
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
2191+
2192+
db.transaction(() => {
2193+
db.exec(`
2194+
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN profileUpdatedSeconds INTEGER;
2195+
`);
2196+
2197+
writeSessionSchemaVersion(targetVersion, db);
2198+
})();
2199+
2200+
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
2201+
}
2202+
21842203
export function printTableColumns(table: string, db: BetterSqlite3.Database) {
21852204
console.info(db.pragma(`table_info('${table}');`));
21862205
}

ts/node/sql.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
476476
groupAdmins,
477477
avatarPointer,
478478
triggerNotificationsFor,
479+
profileUpdatedSeconds,
479480
isTrustedForAttachmentDownload,
480481
isApproved,
481482
didApproveMe,
@@ -528,6 +529,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
528529
groupAdmins: groupAdmins && groupAdmins.length ? arrayStrToJson(groupAdmins) : '[]',
529530
avatarPointer,
530531
triggerNotificationsFor,
532+
profileUpdatedSeconds,
531533
isTrustedForAttachmentDownload: toSqliteBoolean(isTrustedForAttachmentDownload),
532534
priority,
533535
isApproved: toSqliteBoolean(isApproved),

ts/test/session/unit/models/ConversationModels_test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,23 @@ describe('fillConvoAttributesWithDefaults', () => {
129129
});
130130
});
131131

132+
describe('profileUpdatedSeconds', () => {
133+
it('initialize profileUpdatedSeconds if not given', () => {
134+
expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property(
135+
'profileUpdatedSeconds',
136+
0
137+
);
138+
});
139+
140+
it('do not override profileUpdatedSeconds if given', () => {
141+
expect(
142+
fillConvoAttributesWithDefaults({
143+
profileUpdatedSeconds: 1234,
144+
} as ConversationAttributes)
145+
).to.have.deep.property('profileUpdatedSeconds', 1234);
146+
});
147+
});
148+
132149
describe('isTrustedForAttachmentDownload', () => {
133150
it('initialize isTrustedForAttachmentDownload if not given', () => {
134151
expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property(

ts/types/message/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ function extractPicDetailsFromUrl(src: string | null): ProfilePicture {
1515
}
1616
const url = urlParts[0];
1717
const key = urlParts[1];
18+
19+
// throwing here, as if src is not empty we expect a key to be set
1820
if (!isEmpty(key) && !isString(key)) {
1921
throw new Error('extractPicDetailsFromUrl: profileKey is set but not a string');
2022
}
23+
// throwing here, as if src is not empty we expect an url to be set
2124
if (!isEmpty(url) && !isString(url)) {
2225
throw new Error('extractPicDetailsFromUrl: avatarPointer is set but not a string');
2326
}
@@ -46,7 +49,7 @@ class OutgoingUserProfile {
4649
updatedAtSeconds: number;
4750
} & (
4851
| { picUrlWithProfileKey: string | null }
49-
| { profileKey: Uint8Array | null; avatarPointer: string | null }
52+
| { profileKey: Uint8Array | string | null; avatarPointer: string | null }
5053
)) {
5154
if (!isString(displayName)) {
5255
throw new Error('displayName is not a string');
@@ -80,16 +83,21 @@ class OutgoingUserProfile {
8083
}
8184

8285
private initFromPicDetails({
83-
profileKey,
86+
profileKey: profileKeyIn,
8487
avatarPointer,
8588
}: {
86-
profileKey: Uint8Array | null;
89+
profileKey: Uint8Array | string | null;
8790
avatarPointer: string | null;
8891
}) {
89-
if (!profileKey && !avatarPointer) {
92+
if (!profileKeyIn && !avatarPointer) {
9093
this.picUrlWithProfileKey = null;
9194
return;
9295
}
96+
if (profileKeyIn && !isString(profileKeyIn) && !isTypedArray(profileKeyIn)) {
97+
throw new Error('profileKey must be a string or a Uint8Array if set');
98+
}
99+
// check if the profileKey is a string, and if so, convert it to a Uint8Array
100+
const profileKey = isString(profileKeyIn) ? from_hex(profileKeyIn) : profileKeyIn;
93101
if (
94102
!profileKey ||
95103
isEmpty(profileKey) ||

0 commit comments

Comments
 (0)