@massalabs/gossip-sdk 0.0.2-dev.20260220052333 → 0.0.2-dev.20260220053835
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/config/sdk.js +1 -1
- package/dist/contacts.d.ts +11 -17
- package/dist/contacts.js +20 -26
- package/dist/core/SdkEventEmitter.d.ts +2 -0
- package/dist/core/SdkEventEmitter.js +2 -0
- package/dist/core/SdkPolling.d.ts +2 -5
- package/dist/core/SdkPolling.js +1 -4
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -0
- package/dist/db.d.ts +34 -56
- package/dist/db.js +39 -229
- package/dist/gossip.d.ts +54 -23
- package/dist/gossip.js +149 -120
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -4
- package/dist/queries/activeSeekers.d.ts +2 -0
- package/dist/queries/activeSeekers.js +18 -0
- package/dist/queries/announcementCursors.d.ts +2 -0
- package/dist/queries/announcementCursors.js +20 -0
- package/dist/queries/contacts.d.ts +13 -0
- package/dist/queries/contacts.js +43 -0
- package/dist/queries/discussions.d.ts +21 -0
- package/dist/queries/discussions.js +73 -0
- package/dist/queries/index.d.ts +7 -0
- package/dist/queries/index.js +7 -0
- package/dist/queries/messages.d.ts +27 -0
- package/dist/queries/messages.js +120 -0
- package/dist/queries/pendingAnnouncements.d.ts +4 -0
- package/dist/queries/pendingAnnouncements.js +11 -0
- package/dist/queries/userProfile.d.ts +22 -0
- package/dist/queries/userProfile.js +116 -0
- package/dist/schema.d.ts +1280 -0
- package/dist/schema.js +164 -0
- package/dist/services/announcement.d.ts +8 -7
- package/dist/services/announcement.js +95 -121
- package/dist/services/auth.d.ts +1 -3
- package/dist/services/auth.js +4 -11
- package/dist/services/discussion.d.ts +8 -15
- package/dist/services/discussion.js +62 -62
- package/dist/services/message.d.ts +15 -26
- package/dist/services/message.js +282 -270
- package/dist/services/refresh.d.ts +3 -5
- package/dist/services/refresh.js +10 -13
- package/dist/sqlite-worker.d.ts +12 -0
- package/dist/sqlite-worker.js +106 -0
- package/dist/sqlite.d.ts +79 -0
- package/dist/sqlite.js +448 -0
- package/dist/utils/contacts.d.ts +2 -5
- package/dist/utils/contacts.js +23 -39
- package/dist/utils/discussions.d.ts +7 -2
- package/dist/utils/discussions.js +24 -5
- package/dist/utils/logs.js +1 -3
- package/dist/utils/validation.d.ts +2 -4
- package/dist/utils/validation.js +5 -10
- package/package.json +5 -7
- package/dist/api/messageProtocol/mock.d.ts +0 -12
- package/dist/api/messageProtocol/mock.js +0 -12
- package/dist/types/events.d.ts +0 -80
- package/dist/types/events.js +0 -7
package/dist/schema.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle ORM schema for the Gossip SDK database.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: The raw DDL in sqlite.ts must stay in sync with these definitions.
|
|
5
|
+
* When adding/removing/renaming columns, tables, or indexes, update BOTH files.
|
|
6
|
+
*
|
|
7
|
+
* Type mappings from SQLite:
|
|
8
|
+
* Date → integer (epoch milliseconds)
|
|
9
|
+
* boolean → integer (0/1)
|
|
10
|
+
* Uint8Array → blob (wa-sqlite handles natively)
|
|
11
|
+
* JSON → text (JSON.stringify/parse in service layer)
|
|
12
|
+
*/
|
|
13
|
+
import { sqliteTable, text, integer, index, uniqueIndex, customType, } from 'drizzle-orm/sqlite-core';
|
|
14
|
+
// Custom blob type — wa-sqlite returns Uint8Array natively for BLOB columns.
|
|
15
|
+
// Drizzle's built-in blob mode converts to/from hex strings, which breaks
|
|
16
|
+
// with wa-sqlite's direct Uint8Array handling. This custom type passes through.
|
|
17
|
+
const bytes = customType({
|
|
18
|
+
dataType() {
|
|
19
|
+
return 'blob';
|
|
20
|
+
},
|
|
21
|
+
fromDriver(value) {
|
|
22
|
+
return value;
|
|
23
|
+
},
|
|
24
|
+
toDriver(value) {
|
|
25
|
+
return value;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// contacts
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
export const contacts = sqliteTable('contacts', {
|
|
32
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
33
|
+
ownerUserId: text('ownerUserId').notNull(),
|
|
34
|
+
userId: text('userId').notNull(),
|
|
35
|
+
name: text('name').notNull(),
|
|
36
|
+
avatar: text('avatar'),
|
|
37
|
+
publicKeys: bytes('publicKeys').notNull(),
|
|
38
|
+
isOnline: integer('isOnline', { mode: 'boolean' }).notNull(),
|
|
39
|
+
lastSeen: integer('lastSeen', { mode: 'timestamp_ms' }).notNull(),
|
|
40
|
+
createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
|
|
41
|
+
}, table => [
|
|
42
|
+
index('contacts_owner_user_idx').on(table.ownerUserId, table.userId),
|
|
43
|
+
index('contacts_owner_name_idx').on(table.ownerUserId, table.name),
|
|
44
|
+
]);
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// messages
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export const messages = sqliteTable('messages', {
|
|
49
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
50
|
+
ownerUserId: text('ownerUserId').notNull(),
|
|
51
|
+
contactUserId: text('contactUserId').notNull(),
|
|
52
|
+
messageId: bytes('messageId'), // 12-byte random ID for deduplication
|
|
53
|
+
content: text('content').notNull(),
|
|
54
|
+
serializedContent: bytes('serializedContent'),
|
|
55
|
+
type: text('type').$type().notNull(),
|
|
56
|
+
direction: text('direction').$type().notNull(),
|
|
57
|
+
status: text('status').$type().notNull(),
|
|
58
|
+
timestamp: integer('timestamp', { mode: 'timestamp_ms' }).notNull(),
|
|
59
|
+
metadata: text('metadata'), // JSON — service layer handles stringify/parse
|
|
60
|
+
seeker: bytes('seeker'),
|
|
61
|
+
replyTo: text('replyTo'), // JSON — contains Uint8Array fields (base64 encoded)
|
|
62
|
+
forwardOf: text('forwardOf'), // JSON — contains Uint8Array fields (base64 encoded)
|
|
63
|
+
encryptedMessage: bytes('encryptedMessage'),
|
|
64
|
+
whenToSend: integer('whenToSend', { mode: 'timestamp_ms' }),
|
|
65
|
+
}, table => [
|
|
66
|
+
index('messages_owner_contact_idx').on(table.ownerUserId, table.contactUserId),
|
|
67
|
+
index('messages_owner_status_idx').on(table.ownerUserId, table.status),
|
|
68
|
+
index('messages_owner_contact_status_idx').on(table.ownerUserId, table.contactUserId, table.status),
|
|
69
|
+
index('messages_owner_seeker_idx').on(table.ownerUserId, table.seeker),
|
|
70
|
+
index('messages_owner_contact_dir_idx').on(table.ownerUserId, table.contactUserId, table.direction),
|
|
71
|
+
index('messages_owner_dir_status_idx').on(table.ownerUserId, table.direction, table.status),
|
|
72
|
+
index('messages_timestamp_idx').on(table.timestamp),
|
|
73
|
+
]);
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// userProfile
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export const userProfile = sqliteTable('userProfile', {
|
|
78
|
+
userId: text('userId').primaryKey(),
|
|
79
|
+
username: text('username').notNull(),
|
|
80
|
+
avatar: text('avatar'),
|
|
81
|
+
bio: text('bio'),
|
|
82
|
+
status: text('status').notNull(),
|
|
83
|
+
lastSeen: integer('lastSeen', { mode: 'timestamp_ms' }).notNull(),
|
|
84
|
+
createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
|
|
85
|
+
updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),
|
|
86
|
+
lastPublicKeyPush: integer('lastPublicKeyPush', {
|
|
87
|
+
mode: 'timestamp_ms',
|
|
88
|
+
}),
|
|
89
|
+
security: text('security').notNull(), // JSON — contains Uint8Array fields (base64 encoded)
|
|
90
|
+
session: bytes('session').notNull(),
|
|
91
|
+
}, table => [
|
|
92
|
+
index('userProfile_username_idx').on(table.username),
|
|
93
|
+
index('userProfile_status_idx').on(table.status),
|
|
94
|
+
]);
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// discussions
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export const discussions = sqliteTable('discussions', {
|
|
99
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
100
|
+
ownerUserId: text('ownerUserId').notNull(),
|
|
101
|
+
contactUserId: text('contactUserId').notNull(),
|
|
102
|
+
weAccepted: integer('weAccepted', { mode: 'boolean' })
|
|
103
|
+
.notNull()
|
|
104
|
+
.default(false),
|
|
105
|
+
sendAnnouncement: text('sendAnnouncement'), // JSON — nullable
|
|
106
|
+
direction: text('direction').$type().notNull(),
|
|
107
|
+
status: text('status').$type().notNull(),
|
|
108
|
+
nextSeeker: bytes('nextSeeker'),
|
|
109
|
+
initiationAnnouncement: bytes('initiationAnnouncement'),
|
|
110
|
+
announcementMessage: text('announcementMessage'),
|
|
111
|
+
lastSyncTimestamp: integer('lastSyncTimestamp', {
|
|
112
|
+
mode: 'timestamp_ms',
|
|
113
|
+
}),
|
|
114
|
+
customName: text('customName'),
|
|
115
|
+
lastMessageId: integer('lastMessageId'),
|
|
116
|
+
lastMessageContent: text('lastMessageContent'),
|
|
117
|
+
lastMessageTimestamp: integer('lastMessageTimestamp', {
|
|
118
|
+
mode: 'timestamp_ms',
|
|
119
|
+
}),
|
|
120
|
+
unreadCount: integer('unreadCount').notNull().default(0),
|
|
121
|
+
createdAt: integer('createdAt', { mode: 'timestamp_ms' }).notNull(),
|
|
122
|
+
updatedAt: integer('updatedAt', { mode: 'timestamp_ms' }).notNull(),
|
|
123
|
+
}, table => [
|
|
124
|
+
uniqueIndex('discussions_owner_contact_idx').on(table.ownerUserId, table.contactUserId),
|
|
125
|
+
index('discussions_owner_status_idx').on(table.ownerUserId, table.status),
|
|
126
|
+
]);
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// pendingEncryptedMessages
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export const pendingEncryptedMessages = sqliteTable('pendingEncryptedMessages', {
|
|
131
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
132
|
+
seeker: bytes('seeker').notNull(),
|
|
133
|
+
ciphertext: bytes('ciphertext').notNull(),
|
|
134
|
+
fetchedAt: integer('fetchedAt', { mode: 'timestamp_ms' }).notNull(),
|
|
135
|
+
}, table => [
|
|
136
|
+
index('pending_encrypted_seeker_idx').on(table.seeker),
|
|
137
|
+
index('pending_encrypted_fetchedAt_idx').on(table.fetchedAt),
|
|
138
|
+
]);
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// pendingAnnouncements
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
export const pendingAnnouncements = sqliteTable('pendingAnnouncements', {
|
|
143
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
144
|
+
announcement: bytes('announcement').notNull(),
|
|
145
|
+
fetchedAt: integer('fetchedAt', { mode: 'timestamp_ms' }).notNull(),
|
|
146
|
+
counter: text('counter'),
|
|
147
|
+
}, table => [
|
|
148
|
+
uniqueIndex('pending_announcements_announcement_idx').on(table.announcement),
|
|
149
|
+
index('pending_announcements_fetchedAt_idx').on(table.fetchedAt),
|
|
150
|
+
]);
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// activeSeekers
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
export const activeSeekers = sqliteTable('activeSeekers', {
|
|
155
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
156
|
+
seeker: bytes('seeker').notNull(),
|
|
157
|
+
}, table => [index('active_seekers_seeker_idx').on(table.seeker)]);
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// announcementCursors
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
export const announcementCursors = sqliteTable('announcementCursors', {
|
|
162
|
+
userId: text('userId').primaryKey(),
|
|
163
|
+
counter: text('counter').notNull(),
|
|
164
|
+
});
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles broadcasting and processing of session announcements.
|
|
5
5
|
*/
|
|
6
|
-
import { type Discussion
|
|
6
|
+
import { type Discussion } from '../db';
|
|
7
7
|
import { IMessageProtocol } from '../api/messageProtocol';
|
|
8
8
|
import { UserPublicKeys } from '../wasm/bindings';
|
|
9
9
|
import { SessionModule } from '../wasm/session';
|
|
10
10
|
import { SdkConfig } from '../config/sdk';
|
|
11
11
|
import { Result } from '../utils/type';
|
|
12
12
|
import { SdkEventEmitter } from '../core/SdkEventEmitter';
|
|
13
|
+
import type { RefreshService } from './refresh';
|
|
13
14
|
export interface AnnouncementReceptionResult {
|
|
14
15
|
success: boolean;
|
|
15
16
|
newAnnouncementsCount: number;
|
|
@@ -17,13 +18,14 @@ export interface AnnouncementReceptionResult {
|
|
|
17
18
|
}
|
|
18
19
|
export declare const EstablishSessionError = "Session manager failed to establish outgoing session";
|
|
19
20
|
export declare class AnnouncementService {
|
|
20
|
-
private db;
|
|
21
21
|
private messageProtocol;
|
|
22
22
|
private session;
|
|
23
23
|
private isProcessingAnnouncements;
|
|
24
24
|
private eventEmitter;
|
|
25
25
|
private config;
|
|
26
|
-
|
|
26
|
+
private refreshService?;
|
|
27
|
+
constructor(messageProtocol: IMessageProtocol, session: SessionModule, eventEmitter: SdkEventEmitter, config?: SdkConfig);
|
|
28
|
+
setRefreshService(refreshService: RefreshService): void;
|
|
27
29
|
setMessageProtocol(messageProtocol: IMessageProtocol): void;
|
|
28
30
|
sendAnnouncement(announcement: Uint8Array): Promise<{
|
|
29
31
|
success: boolean;
|
|
@@ -34,15 +36,14 @@ export declare class AnnouncementService {
|
|
|
34
36
|
processOutgoingAnnouncements(discussions: Discussion[]): Promise<void>;
|
|
35
37
|
fetchAndProcessAnnouncements(): Promise<AnnouncementReceptionResult>;
|
|
36
38
|
/**
|
|
37
|
-
* Persist
|
|
38
|
-
*
|
|
39
|
-
* since a row without `security`/`session` would crash downstream code.
|
|
39
|
+
* Persist the bulletin polling cursor for the current user.
|
|
40
|
+
* Stored in a dedicated announcementCursors table (not userProfile).
|
|
40
41
|
*/
|
|
41
42
|
private _upsertLastBulletinCounter;
|
|
42
43
|
/**
|
|
43
44
|
* Fetch the latest bulletin counter from the API and persist it so that
|
|
44
45
|
* historical announcements (undecryptable by a new account) are skipped.
|
|
45
|
-
* No-op if a counter already exists
|
|
46
|
+
* No-op if a counter already exists.
|
|
46
47
|
*/
|
|
47
48
|
skipHistoricalAnnouncements(): Promise<void>;
|
|
48
49
|
private _fetchAnnouncements;
|
|
@@ -3,25 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles broadcasting and processing of session announcements.
|
|
5
5
|
*/
|
|
6
|
-
import { DiscussionDirection, MessageDirection, MessageStatus, MessageType, } from '../db';
|
|
6
|
+
import { DiscussionDirection, DiscussionStatus, MessageDirection, MessageStatus, MessageType, serializeSendAnnouncement, } from '../db';
|
|
7
|
+
import { toDiscussion } from '../utils/discussions';
|
|
7
8
|
import { decodeUserId, encodeUserId } from '../utils/userId';
|
|
8
9
|
import { SessionStatus } from '../wasm/bindings';
|
|
9
10
|
import { sessionStatusToString } from '../wasm/session';
|
|
10
11
|
import { Logger } from '../utils/logs';
|
|
11
12
|
import { defaultSdkConfig } from '../config/sdk';
|
|
12
13
|
import { decodeAnnouncementPayload } from '../utils/announcementPayload';
|
|
13
|
-
import { resetSendQueue } from './discussion';
|
|
14
14
|
import { SdkEventType } from '../core/SdkEventEmitter';
|
|
15
|
+
import { getContactByOwnerAndUser, insertContact, getContactNamesByPrefix, getDiscussionByOwnerAndContact, getDiscussionById, insertDiscussion, updateDiscussionById, insertMessage, getAnnouncementMessagesByContact, resetSendQueueMessages, getAllPendingAnnouncements, deletePendingAnnouncementsByIds, getAnnouncementCursor, upsertAnnouncementCursor, } from '../queries';
|
|
15
16
|
const logger = new Logger('AnnouncementService');
|
|
16
17
|
export const EstablishSessionError = 'Session manager failed to establish outgoing session';
|
|
17
18
|
export class AnnouncementService {
|
|
18
|
-
constructor(
|
|
19
|
-
Object.defineProperty(this, "db", {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
configurable: true,
|
|
22
|
-
writable: true,
|
|
23
|
-
value: void 0
|
|
24
|
-
});
|
|
19
|
+
constructor(messageProtocol, session, eventEmitter, config = defaultSdkConfig) {
|
|
25
20
|
Object.defineProperty(this, "messageProtocol", {
|
|
26
21
|
enumerable: true,
|
|
27
22
|
configurable: true,
|
|
@@ -52,12 +47,20 @@ export class AnnouncementService {
|
|
|
52
47
|
writable: true,
|
|
53
48
|
value: void 0
|
|
54
49
|
});
|
|
55
|
-
this
|
|
50
|
+
Object.defineProperty(this, "refreshService", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: void 0
|
|
55
|
+
});
|
|
56
56
|
this.messageProtocol = messageProtocol;
|
|
57
57
|
this.session = session;
|
|
58
58
|
this.eventEmitter = eventEmitter;
|
|
59
59
|
this.config = config;
|
|
60
60
|
}
|
|
61
|
+
setRefreshService(refreshService) {
|
|
62
|
+
this.refreshService = refreshService;
|
|
63
|
+
}
|
|
61
64
|
setMessageProtocol(messageProtocol) {
|
|
62
65
|
this.messageProtocol = messageProtocol;
|
|
63
66
|
}
|
|
@@ -100,7 +103,7 @@ export class AnnouncementService {
|
|
|
100
103
|
for (const discussion of discussions) {
|
|
101
104
|
if (!discussion.id)
|
|
102
105
|
continue;
|
|
103
|
-
//
|
|
106
|
+
// sendAnnouncement is deserialized by toDiscussion() before reaching here
|
|
104
107
|
if (discussion.sendAnnouncement === null)
|
|
105
108
|
continue;
|
|
106
109
|
const { announcement_bytes, when_to_send } = discussion.sendAnnouncement;
|
|
@@ -109,7 +112,7 @@ export class AnnouncementService {
|
|
|
109
112
|
discussionId: discussion.id,
|
|
110
113
|
contactUserId: discussion.contactUserId,
|
|
111
114
|
});
|
|
112
|
-
await
|
|
115
|
+
await updateDiscussionById(discussion.id, {
|
|
113
116
|
sendAnnouncement: null,
|
|
114
117
|
updatedAt: new Date(),
|
|
115
118
|
});
|
|
@@ -126,28 +129,26 @@ export class AnnouncementService {
|
|
|
126
129
|
}
|
|
127
130
|
// Send the announcement and handle updates after transaction
|
|
128
131
|
const result = await this.sendAnnouncement(announcement_bytes);
|
|
129
|
-
// update discussion state after sending
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
});
|
|
132
|
+
// update discussion state after sending
|
|
133
|
+
const latest = await getDiscussionById(discussion.id);
|
|
134
|
+
if (!latest || latest.sendAnnouncement === null) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (result.success) {
|
|
138
|
+
await updateDiscussionById(discussion.id, {
|
|
139
|
+
sendAnnouncement: null,
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
await updateDiscussionById(discussion.id, {
|
|
145
|
+
sendAnnouncement: serializeSendAnnouncement({
|
|
146
|
+
announcement_bytes,
|
|
147
|
+
when_to_send: new Date(Date.now() + this.config.announcements.retryDelayMs),
|
|
148
|
+
}),
|
|
149
|
+
updatedAt: new Date(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
async fetchAndProcessAnnouncements() {
|
|
@@ -161,10 +162,10 @@ export class AnnouncementService {
|
|
|
161
162
|
let fetchedCounters = [];
|
|
162
163
|
this.isProcessingAnnouncements = true;
|
|
163
164
|
try {
|
|
164
|
-
const pending = await
|
|
165
|
+
const pending = await getAllPendingAnnouncements();
|
|
165
166
|
const successfullyProcessedPendingIds = [];
|
|
166
167
|
if (pending.length > 0) {
|
|
167
|
-
log.info(`processing ${pending.length} pending announcements from
|
|
168
|
+
log.info(`processing ${pending.length} pending announcements from SQLite`);
|
|
168
169
|
// Process pending announcements one by one, tracking successes
|
|
169
170
|
let newAnnouncementsCount = 0;
|
|
170
171
|
for (const item of pending) {
|
|
@@ -197,7 +198,7 @@ export class AnnouncementService {
|
|
|
197
198
|
}
|
|
198
199
|
// Delete only successfully processed pending announcements
|
|
199
200
|
if (successfullyProcessedPendingIds.length > 0) {
|
|
200
|
-
await
|
|
201
|
+
await deletePendingAnnouncementsByIds(successfullyProcessedPendingIds);
|
|
201
202
|
log.info(`deleted ${successfullyProcessedPendingIds.length} processed pending announcements`);
|
|
202
203
|
}
|
|
203
204
|
if (fetchedCounters.length > 0) {
|
|
@@ -212,8 +213,7 @@ export class AnnouncementService {
|
|
|
212
213
|
};
|
|
213
214
|
}
|
|
214
215
|
// No pending - fetch from API
|
|
215
|
-
const cursor =
|
|
216
|
-
?.lastBulletinCounter;
|
|
216
|
+
const cursor = await getAnnouncementCursor(this.session.userIdEncoded);
|
|
217
217
|
// fetch from node all announcements since the last retrieved announcement
|
|
218
218
|
const fetched = await this._fetchAnnouncements(cursor);
|
|
219
219
|
announcements = fetched.map(a => a.data);
|
|
@@ -266,37 +266,21 @@ export class AnnouncementService {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
/**
|
|
269
|
-
* Persist
|
|
270
|
-
*
|
|
271
|
-
* since a row without `security`/`session` would crash downstream code.
|
|
269
|
+
* Persist the bulletin polling cursor for the current user.
|
|
270
|
+
* Stored in a dedicated announcementCursors table (not userProfile).
|
|
272
271
|
*/
|
|
273
272
|
async _upsertLastBulletinCounter(nextCounter) {
|
|
274
|
-
|
|
275
|
-
const existing = await this.db.userProfile.get(userId);
|
|
276
|
-
if (!existing) {
|
|
277
|
-
logger
|
|
278
|
-
.forMethod('_upsertLastBulletinCounter')
|
|
279
|
-
.debug('no profile row yet — skipping counter write');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
await this.db.userProfile.update(userId, {
|
|
283
|
-
lastBulletinCounter: nextCounter,
|
|
284
|
-
updatedAt: new Date(),
|
|
285
|
-
});
|
|
273
|
+
await upsertAnnouncementCursor(this.session.userIdEncoded, nextCounter);
|
|
286
274
|
}
|
|
287
275
|
/**
|
|
288
276
|
* Fetch the latest bulletin counter from the API and persist it so that
|
|
289
277
|
* historical announcements (undecryptable by a new account) are skipped.
|
|
290
|
-
* No-op if a counter already exists
|
|
278
|
+
* No-op if a counter already exists.
|
|
291
279
|
*/
|
|
292
280
|
async skipHistoricalAnnouncements() {
|
|
293
281
|
const log = logger.forMethod('skipHistoricalAnnouncements');
|
|
294
|
-
const existing = await
|
|
295
|
-
if (
|
|
296
|
-
log.debug('no profile row yet — skipping');
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (existing.lastBulletinCounter !== undefined)
|
|
282
|
+
const existing = await getAnnouncementCursor(this.session.userIdEncoded);
|
|
283
|
+
if (existing !== undefined)
|
|
300
284
|
return;
|
|
301
285
|
try {
|
|
302
286
|
const counter = await this.messageProtocol.fetchBulletinCounter();
|
|
@@ -321,11 +305,7 @@ export class AnnouncementService {
|
|
|
321
305
|
}
|
|
322
306
|
}
|
|
323
307
|
async _generateTemporaryContactName(ownerUserId) {
|
|
324
|
-
const newRequestContacts = await
|
|
325
|
-
.where('ownerUserId')
|
|
326
|
-
.equals(ownerUserId)
|
|
327
|
-
.filter(contact => contact.name.startsWith('New Request'))
|
|
328
|
-
.toArray();
|
|
308
|
+
const newRequestContacts = await getContactNamesByPrefix(ownerUserId, 'New Request');
|
|
329
309
|
const numbers = newRequestContacts
|
|
330
310
|
.map(contact => {
|
|
331
311
|
const match = contact.name.match(/^New Request (\d+)$/);
|
|
@@ -349,29 +329,26 @@ export class AnnouncementService {
|
|
|
349
329
|
const contactUserIdRaw = announcerPkeys.derive_id();
|
|
350
330
|
const contactUserId = encodeUserId(contactUserIdRaw);
|
|
351
331
|
const sessionStatus = this.session.peerSessionStatus(contactUserIdRaw);
|
|
352
|
-
|
|
353
|
-
console.log(`[Announcement] Received from ${contactUserId.slice(0, 12)}... -> session status: ${sessionStatusToString(sessionStatus)}`);
|
|
354
|
-
log.info('session updated', {
|
|
332
|
+
log.info('announcement received', {
|
|
355
333
|
contactUserId,
|
|
356
334
|
status: sessionStatusToString(sessionStatus),
|
|
357
335
|
});
|
|
358
|
-
let contact = await
|
|
336
|
+
let contact = await getContactByOwnerAndUser(this.session.userIdEncoded, contactUserId);
|
|
359
337
|
const isNewContact = !contact;
|
|
360
338
|
if (isNewContact) {
|
|
361
339
|
// Use extracted username if present, otherwise generate temporary name
|
|
362
340
|
const name = username ||
|
|
363
341
|
(await this._generateTemporaryContactName(this.session.userIdEncoded));
|
|
364
|
-
await
|
|
342
|
+
await insertContact({
|
|
365
343
|
ownerUserId: this.session.userIdEncoded,
|
|
366
344
|
userId: contactUserId,
|
|
367
345
|
name,
|
|
368
346
|
publicKeys: announcerPkeys.to_bytes(),
|
|
369
|
-
avatar: undefined,
|
|
370
347
|
isOnline: false,
|
|
371
348
|
lastSeen: new Date(),
|
|
372
349
|
createdAt: new Date(),
|
|
373
350
|
});
|
|
374
|
-
contact = await
|
|
351
|
+
contact = await getContactByOwnerAndUser(this.session.userIdEncoded, contactUserId);
|
|
375
352
|
log.info('created new contact', { contactUserId, name });
|
|
376
353
|
}
|
|
377
354
|
if (!contact) {
|
|
@@ -384,17 +361,13 @@ export class AnnouncementService {
|
|
|
384
361
|
const timestamp = new Date(result.timestamp);
|
|
385
362
|
const windowStart = new Date(timestamp.getTime() - 1000);
|
|
386
363
|
const windowEnd = new Date(timestamp.getTime() + 1000);
|
|
387
|
-
const existing = await this.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.and(msg => msg.direction === MessageDirection.INCOMING &&
|
|
391
|
-
msg.type === MessageType.ANNOUNCEMENT &&
|
|
392
|
-
msg.content === message &&
|
|
364
|
+
const existing = await getAnnouncementMessagesByContact(this.session.userIdEncoded, contactUserId);
|
|
365
|
+
// Filter in JS for content match and timestamp window (SQLite timestamp_ms comparison)
|
|
366
|
+
const duplicate = existing.find(msg => msg.content === message &&
|
|
393
367
|
msg.timestamp >= windowStart &&
|
|
394
|
-
msg.timestamp <= windowEnd)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
await this.db.addMessage({
|
|
368
|
+
msg.timestamp <= windowEnd);
|
|
369
|
+
if (!duplicate) {
|
|
370
|
+
await insertMessage({
|
|
398
371
|
ownerUserId: this.session.userIdEncoded,
|
|
399
372
|
contactUserId,
|
|
400
373
|
content: message,
|
|
@@ -405,6 +378,7 @@ export class AnnouncementService {
|
|
|
405
378
|
});
|
|
406
379
|
}
|
|
407
380
|
}
|
|
381
|
+
await this.refreshService?.stateUpdate();
|
|
408
382
|
return {
|
|
409
383
|
success: true,
|
|
410
384
|
discussionId,
|
|
@@ -413,47 +387,47 @@ export class AnnouncementService {
|
|
|
413
387
|
}
|
|
414
388
|
async _handleReceivedDiscussion(ownerUserId, contactUserId, message) {
|
|
415
389
|
const log = logger.forMethod('handleReceivedDiscussion');
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
discussionId: existing.id,
|
|
424
|
-
contactUserId,
|
|
425
|
-
});
|
|
426
|
-
// Group update and queue reset in the same transaction to ensure atomicity
|
|
427
|
-
await this.db.discussions.update(existing.id, updateData);
|
|
428
|
-
// reset all messages in send queue to WAITING_SESSION for this contact
|
|
429
|
-
await resetSendQueue(this.db, this.session.userIdEncoded, contactUserId);
|
|
430
|
-
const newDiscussion = await this.db.discussions.get(existing.id);
|
|
431
|
-
const contact = await this.db.getContactByOwnerAndUserId(ownerUserId, contactUserId);
|
|
432
|
-
if (newDiscussion &&
|
|
433
|
-
contact &&
|
|
434
|
-
this.session.peerSessionStatus(decodeUserId(contactUserId)) ===
|
|
435
|
-
SessionStatus.PeerRequested) {
|
|
436
|
-
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, newDiscussion, contact);
|
|
437
|
-
}
|
|
438
|
-
return existing.id;
|
|
439
|
-
}
|
|
440
|
-
log.info('creating new discussion', { contactUserId });
|
|
441
|
-
return await this.db.discussions.add({
|
|
442
|
-
ownerUserId,
|
|
390
|
+
const existing = await getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
|
|
391
|
+
if (existing) {
|
|
392
|
+
const updateData = { updatedAt: new Date() };
|
|
393
|
+
if (message)
|
|
394
|
+
updateData.announcementMessage = message;
|
|
395
|
+
log.info('updating existing discussion', {
|
|
396
|
+
discussionId: existing.id,
|
|
443
397
|
contactUserId,
|
|
444
|
-
weAccepted: false,
|
|
445
|
-
sendAnnouncement: null,
|
|
446
|
-
direction: DiscussionDirection.RECEIVED,
|
|
447
|
-
lastAnnouncementMessage: message,
|
|
448
|
-
unreadCount: 0,
|
|
449
|
-
createdAt: new Date(),
|
|
450
|
-
updatedAt: new Date(),
|
|
451
398
|
});
|
|
399
|
+
await updateDiscussionById(existing.id, updateData);
|
|
400
|
+
// reset pending outgoing messages to WAITING_SESSION for this contact
|
|
401
|
+
// Only reset READY and SENT — DELIVERED messages should not be resent.
|
|
402
|
+
await resetSendQueueMessages(this.session.userIdEncoded, contactUserId, [
|
|
403
|
+
MessageStatus.READY,
|
|
404
|
+
MessageStatus.SENT,
|
|
405
|
+
]);
|
|
406
|
+
const newDiscussion = await getDiscussionById(existing.id);
|
|
407
|
+
const contactRow = await getContactByOwnerAndUser(ownerUserId, contactUserId);
|
|
408
|
+
if (newDiscussion &&
|
|
409
|
+
contactRow &&
|
|
410
|
+
this.session.peerSessionStatus(decodeUserId(contactUserId)) ===
|
|
411
|
+
SessionStatus.PeerRequested) {
|
|
412
|
+
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, toDiscussion(newDiscussion), contactRow);
|
|
413
|
+
}
|
|
414
|
+
return { discussionId: existing.id };
|
|
415
|
+
}
|
|
416
|
+
log.info('creating new discussion', { contactUserId });
|
|
417
|
+
const discussionId = await insertDiscussion({
|
|
418
|
+
ownerUserId,
|
|
419
|
+
contactUserId,
|
|
420
|
+
direction: DiscussionDirection.RECEIVED,
|
|
421
|
+
status: DiscussionStatus.PENDING,
|
|
422
|
+
announcementMessage: message,
|
|
423
|
+
unreadCount: 0,
|
|
424
|
+
createdAt: new Date(),
|
|
425
|
+
updatedAt: new Date(),
|
|
452
426
|
});
|
|
453
|
-
const discussion = await
|
|
454
|
-
const
|
|
455
|
-
if (discussion &&
|
|
456
|
-
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, discussion,
|
|
427
|
+
const discussion = await getDiscussionById(discussionId);
|
|
428
|
+
const contactRow = await getContactByOwnerAndUser(ownerUserId, contactUserId);
|
|
429
|
+
if (discussion && contactRow) {
|
|
430
|
+
this.eventEmitter.emit(SdkEventType.SESSION_REQUESTED, toDiscussion(discussion), contactRow);
|
|
457
431
|
}
|
|
458
432
|
return { discussionId };
|
|
459
433
|
}
|
package/dist/services/auth.d.ts
CHANGED
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { UserPublicKeys } from '../wasm/bindings';
|
|
7
7
|
import { IMessageProtocol } from '../api/messageProtocol/types';
|
|
8
|
-
import { type GossipDatabase } from '../db';
|
|
9
8
|
export declare class AuthService {
|
|
10
|
-
private db;
|
|
11
9
|
messageProtocol: IMessageProtocol;
|
|
12
|
-
constructor(
|
|
10
|
+
constructor(messageProtocol: IMessageProtocol);
|
|
13
11
|
/**
|
|
14
12
|
* Fetch public key by userId
|
|
15
13
|
* @param userId - Bech32-encoded userId (e.g., "gossip1...")
|
package/dist/services/auth.js
CHANGED
|
@@ -6,14 +6,9 @@
|
|
|
6
6
|
import { UserPublicKeys } from '../wasm/bindings';
|
|
7
7
|
import { decodeUserId } from '../utils/userId';
|
|
8
8
|
import { encodeToBase64, decodeFromBase64 } from '../utils/base64';
|
|
9
|
+
import { getUserProfileField, updateUserProfileById } from '../queries';
|
|
9
10
|
export class AuthService {
|
|
10
|
-
constructor(
|
|
11
|
-
Object.defineProperty(this, "db", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
configurable: true,
|
|
14
|
-
writable: true,
|
|
15
|
-
value: db
|
|
16
|
-
});
|
|
11
|
+
constructor(messageProtocol) {
|
|
17
12
|
Object.defineProperty(this, "messageProtocol", {
|
|
18
13
|
enumerable: true,
|
|
19
14
|
configurable: true,
|
|
@@ -41,7 +36,7 @@ export class AuthService {
|
|
|
41
36
|
* @param userId - Bech32-encoded userId (e.g., "gossip1...")
|
|
42
37
|
*/
|
|
43
38
|
async ensurePublicKeyPublished(publicKeys, userId) {
|
|
44
|
-
const profile = await
|
|
39
|
+
const profile = await getUserProfileField(userId);
|
|
45
40
|
if (profile) {
|
|
46
41
|
const lastPush = profile.lastPublicKeyPush;
|
|
47
42
|
if (lastPush && !moreThanOneWeekAgo(lastPush)) {
|
|
@@ -50,9 +45,7 @@ export class AuthService {
|
|
|
50
45
|
}
|
|
51
46
|
await this.messageProtocol.postPublicKey(encodeToBase64(publicKeys.to_bytes()));
|
|
52
47
|
if (profile) {
|
|
53
|
-
await
|
|
54
|
-
lastPublicKeyPush: new Date(),
|
|
55
|
-
});
|
|
48
|
+
await updateUserProfileById(userId, { lastPublicKeyPush: new Date() });
|
|
56
49
|
}
|
|
57
50
|
}
|
|
58
51
|
}
|