• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

karanshukla / navyfragen-app / 28455186937

30 Jun 2026 03:15PM UTC coverage: 96.163% (-1.4%) from 97.563%
28455186937

push

github

web-flow
Merge pull request #191 from karanshukla/dev-push-notifications

Dev push notifications

2103 of 2293 branches covered (91.71%)

Branch coverage included in aggregate %.

880 of 988 new or added lines in 20 files covered. (89.07%)

4 existing lines in 4 files now uncovered.

7923 of 8133 relevant lines covered (97.42%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.47
/server/src/controllers/message-controller.ts
1
/* v8 ignore start */
1✔
2
import express from "express";
1✔
3
import { body } from "express-validator";
1✔
4
import { Logger } from "pino";
1✔
5

1✔
6
import { MessageService } from "../services/message-service";
1✔
7
import { NotificationService } from "../services/notification-service";
1✔
8

1✔
9
import type { AppContext } from "../index";
1✔
10

1✔
11
import { initializeAgentFromSession } from "#/auth/session-agent";
1✔
12

1✔
13
export class MessageController {
1✔
14
  constructor(
1✔
15
    private messageService: MessageService,
1✔
16
    private logger: Logger,
1✔
17
    private ctx: AppContext,
1✔
18
    /**
1✔
19
     * Optional push-notification side-effect. When wired (production routes),
1✔
20
     * it triggers a push on new messages and drops subscriptions on account
1✔
21
     * deletion. Optional + fire-and-forget so it never blocks a request, and
1✔
22
     * existing call sites that omit it keep working unchanged.
1✔
23
     */
1✔
24
    private notificationService?: NotificationService
1✔
25
  ) {}
1✔
26
  /* v8 ignore stop */
1✔
27

40✔
28
  /**
40✔
29
   * Validation for adding example messages
40✔
30
   */
40✔
31
  validateAddExampleMessages = [
40✔
32
    body("recipient").isString().notEmpty().withMessage("Recipient DID required"),
40✔
33
  ];
40✔
34

40✔
35
  /**
40✔
36
   * Add example messages for a user
40✔
37
   */
40✔
38
  addExampleMessages = async (
40✔
39
    req: express.Request,
3✔
40
    res: express.Response
3✔
41
  ): Promise<express.Response> => {
3✔
42
    const recipient = req.session?.did;
3✔
43
    if (!recipient) {
3✔
44
      return res.status(403).json({ error: "Recipient DID required" });
1✔
45
    }
1✔
46

3✔
47
    try {
3✔
48
      const messages = await this.messageService.addExampleMessages(recipient);
2✔
49
      return res.json({ messages });
3✔
50
    } catch (err) {
1✔
51
      this.logger.error({ err, recipient }, "Failed to add example messages");
1✔
52
      return res.status(500).json({ error: "Failed to add example messages" });
1✔
53
    }
1✔
54
  };
3✔
55

40✔
56
  /**
40✔
57
   * Validation for responding to a message
40✔
58
   */
40✔
59
  validateRespondToMessage = [
40✔
60
    body("tid").isString().notEmpty().withMessage("Message TID required"),
40✔
61
    body("recipient").isString().notEmpty().withMessage("Recipient DID required"),
40✔
62
    body("original").isString().notEmpty().withMessage("Original message required"),
40✔
63
    body("response")
40✔
64
      .isString()
40✔
65
      .isLength({ min: 1, max: 500 })
40✔
66
      .withMessage("Response must be 1-500 chars"),
40✔
67
    body("includeQuestionAsImage").isBoolean().optional(),
40✔
68
    body("replyTo").optional().isObject(),
40✔
69
    body("replyTo.uri").optional().isString(),
40✔
70
    body("replyTo.cid").optional().isString(),
40✔
71
  ];
40✔
72

40✔
73
  /**
40✔
74
   * Respond to a message and post to Bluesky
40✔
75
   */
40✔
76
  respondToMessage = async (
40✔
77
    req: express.Request,
7✔
78
    res: express.Response
7✔
79
  ): Promise<express.Response> => {
7✔
80
    const { tid, recipient, original, response, includeQuestionAsImage, replyTo } = req.body;
7✔
81

7✔
82
    if (!tid || !recipient || !response) {
7✔
83
      this.logger.warn({ tid, recipient, response }, "Missing required fields in respond endpoint");
1✔
84
      return res.status(400).json({ error: "Missing required fields" });
1✔
85
    }
1✔
86

7✔
87
    const did = req.session?.did;
7✔
88
    if (!did) {
7✔
89
      this.logger.warn("No authenticated user session found");
1✔
90
      return res.status(403).json({ error: "Not authenticated" });
1✔
91
    }
1✔
92

7✔
93
    const agent = await initializeAgentFromSession(req, this.ctx);
7✔
94
    if (!agent) {
7✔
95
      this.logger.warn({ did }, "No agent could be initialized from session");
1✔
96
      return res.json({ isLoggedIn: false, profile: null, did: null });
1✔
97
    }
1✔
98

7✔
99
    try {
7✔
100
      const result = await this.messageService.respondToMessage(
4✔
101
        tid,
4✔
102
        did,
4✔
103
        recipient,
4✔
104
        original,
4✔
105
        response,
4✔
106
        includeQuestionAsImage || false,
7✔
107
        agent,
7✔
108
        replyTo
7✔
109
      );
7✔
110
      return res.json(result);
7✔
111
    } catch (err: any) {
2✔
112
      this.logger.error(
2✔
113
        { err, tid, did },
2✔
114
        "Error in /messages/respond endpoint while trying to post to Bluesky"
2✔
115
      );
2✔
116
      return res.status(500).json({ error: err.message || "Failed to post to Bluesky" });
2✔
117
    }
2✔
118
  };
7✔
119

40✔
120
  /**
40✔
121
   * Validation for sending a message
40✔
122
   */
40✔
123
  validateSendMessage = [
40✔
124
    body("recipient").isString().notEmpty().withMessage("Recipient DID required"),
40✔
125
    body("message")
40✔
126
      .isString()
40✔
127
      .isLength({ min: 1, max: 500 })
40✔
128
      .withMessage("Message must be 1-500 chars"),
40✔
129
  ];
40✔
130

40✔
131
  /**
40✔
132
   * Send an anonymous message
40✔
133
   */
40✔
134
  sendMessage = async (req: express.Request, res: express.Response): Promise<express.Response> => {
40✔
135
    const { recipient, message } = req.body;
6✔
136

6✔
137
    if (!recipient || !message) {
6✔
138
      return res.status(400).json({ error: "Recipient and message required" });
1✔
139
    }
1✔
140

6✔
141
    try {
6✔
142
      const result = await this.messageService.sendMessage(recipient, message);
5✔
143
      this.logger.info({ recipient }, "Anonymous message sent");
6✔
144
      // Fire-and-forget: a push failure must never affect the send response.
2✔
145
      this.notificationService
2✔
146
        ?.sendNewMessageNotification(recipient)
6✔
147
        .catch((err) =>
6✔
NEW
148
          this.logger.error({ err, did: recipient }, "Failed to send push notification")
×
149
        );
6✔
150
      return res.json(result);
6✔
151
    } catch (err: any) {
6✔
152
      this.logger.error({ err, recipient }, "Failed to send message");
3✔
153
      return res.status(err.message.includes("not found") ? 404 : 500).json({
3✔
154
        error: err.message || "Failed to send message",
3✔
155
      });
3✔
156
    }
3✔
157
  };
6✔
158

40✔
159
  /**
40✔
160
   * Get messages for a user
40✔
161
   */
40✔
162
  getMessages = async (req: express.Request, res: express.Response): Promise<express.Response> => {
40✔
163
    const recipient = req.session?.did;
4✔
164

4✔
165
    if (!recipient) {
4✔
166
      return res.status(403).json({ error: "Not authenticated" });
1✔
167
    }
1✔
168

4✔
169
    try {
4✔
170
      const messages = await this.messageService.getMessages(recipient);
3✔
171
      return res.json({ messages });
4✔
172
    } catch (err: any) {
4✔
173
      if (err.message.includes("not exist")) {
2✔
174
        return res.status(404).json({ error: "User not found (user profile does not exist)" });
1✔
175
      }
1✔
176
      this.logger.error({ err, recipient }, "Failed to fetch messages");
1✔
177
      return res.status(500).json({ error: "Failed to fetch messages" });
1✔
178
    }
1✔
179
  };
4✔
180

40✔
181
  /**
40✔
182
   * Delete a message
40✔
183
   */
40✔
184
  deleteMessage = async (
40✔
185
    req: express.Request,
8✔
186
    res: express.Response
8✔
187
  ): Promise<express.Response> => {
8✔
188
    const { tid } = req.params;
8✔
189

8✔
190
    if (!tid) {
8✔
191
      return res.status(400).json({ error: "Message TID required" });
1✔
192
    }
1✔
193

8✔
194
    const userSessionDid = req.session?.did;
8✔
195
    if (!userSessionDid) {
8✔
196
      return res.status(403).json({ error: "Not authenticated" });
1✔
197
    }
1✔
198

8✔
199
    const agent = await initializeAgentFromSession(req, this.ctx);
8✔
200
    if (!agent) {
8✔
201
      this.logger.warn({ userSessionDid }, "No agent could be initialized from session");
1✔
202
      return res.json({ isLoggedIn: false, profile: null, did: null });
1✔
203
    }
1✔
204

8✔
205
    try {
8✔
206
      await this.messageService.deleteMessage(tid, userSessionDid, agent);
5✔
207
      return res.json({ success: true });
8✔
208
    } catch (err: any) {
8✔
209
      const status = err.message.includes("not found")
4✔
210
        ? 404
1✔
211
        : err.message.includes("Not authorized")
3✔
212
          ? 403
1✔
213
          : 500;
2✔
214

4✔
215
      return res.status(status).json({ error: err.message || "Failed to delete message" });
4✔
216
    }
4✔
217
  };
8✔
218

40✔
219
  /**
40✔
220
   * Delete all user data
40✔
221
   */
40✔
222
  deleteAccount = async (
40✔
223
    req: express.Request,
6✔
224
    res: express.Response
6✔
225
  ): Promise<express.Response> => {
6✔
226
    const userSessionDid = req.session?.did;
6✔
227

6✔
228
    if (!userSessionDid) {
6✔
229
      return res.status(403).json({ error: "Not authenticated" });
1✔
230
    }
1✔
231

6✔
232
    const agent = await initializeAgentFromSession(req, this.ctx);
6✔
233
    if (!agent) {
6✔
234
      return res.status(401).json({
1✔
235
        error: "Authentication failed - could not initialize agent or retrieve user DID",
1✔
236
      });
1✔
237
    }
1✔
238

6✔
239
    try {
6✔
240
      await this.messageService.deleteUserData(userSessionDid, agent);
4✔
241
      // Fire-and-forget: also drop any push subscriptions for this user.
6✔
242
      this.notificationService
6✔
243
        ?.deleteAllSubscriptionsForUser(userSessionDid)
6✔
244
        .catch((err) =>
6✔
NEW
245
          this.logger.error({ err, did: userSessionDid }, "Failed to delete push subscriptions")
×
246
        );
6✔
247
      req.session = null;
6✔
248
      this.logger.info({ did: userSessionDid }, "Account and all data deleted");
6✔
249
      return res.json({ success: true });
6✔
250
    } catch (err: any) {
6✔
251
      this.logger.error({ err, did: userSessionDid }, "Failed to delete account data");
2✔
252
      return res.status(500).json({ error: err.message || "Failed to delete account data" });
2✔
253
    }
2✔
254
  };
6✔
255

40✔
256
  /**
40✔
257
   * Sync messages to PDS
40✔
258
   */
40✔
259
  syncMessages = async (req: express.Request, res: express.Response): Promise<express.Response> => {
1✔
260
    const userSessionDid = req.session?.did;
6✔
261

6✔
262
    if (!userSessionDid) {
6✔
263
      return res.status(403).json({ error: "Not authenticated" });
1✔
264
    }
1✔
265

6✔
266
    // Stopgap for now
6✔
267
    const userSettings = await this.ctx.db
6✔
268
      .selectFrom("user_settings")
5✔
269
      .selectAll()
5✔
270
      .where("did", "=", userSessionDid)
5✔
271
      .executeTakeFirst();
5✔
272

5✔
273
    if (!userSettings || !userSettings?.pdsSyncEnabled) {
6✔
274
      return res.status(200).json({ success: true, message: "PDS sync is disabled" });
2✔
275
    }
2✔
276

6✔
277
    // Initialize the agent for the authenticated user session
6✔
278
    const agent = await initializeAgentFromSession(req, this.ctx);
6✔
279

3✔
280
    if (!agent) {
6✔
281
      return res.status(401).json({
1✔
282
        error: "Authentication failed - could not initialize agent",
1✔
283
      });
1✔
284
    }
1✔
285

6✔
286
    try {
6✔
287
      const syncResult = await this.messageService.syncMessages(userSessionDid, agent);
2✔
288
      return res.json(syncResult);
6✔
289
    } catch (err: any) {
1✔
290
      this.logger.error({ err, did: userSessionDid }, "Failed to sync messages to PDS");
1✔
291
      return res.status(500).json({
1✔
292
        error: "Failed to sync messages to PDS",
1✔
293
        details: err.message,
1✔
294
      });
1✔
295
    }
1✔
296
  };
6✔
297
  /* v8 ignore next 1 */
1✔
298
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc