leetcode-capture-helper 0.0.0-stage → 0.1.0
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/cli.js +92 -0
- package/config.js +25 -0
- package/framing.js +57 -0
- package/host.js +346 -0
- package/install.js +290 -0
- package/package.json +31 -4
- package/runtime/bundle.json +42 -0
- package/runtime/companion/capture-log.js +83 -0
- package/runtime/companion/codex-backend.js +337 -0
- package/runtime/companion/companion.js +2193 -0
- package/runtime/companion/mouse-input.js +154 -0
- package/runtime/companion/package.json +4 -0
- package/runtime/companion/panel-bridge.js +273 -0
- package/runtime/companion/settings-rpc.js +228 -0
- package/runtime/companion/settings.js +464 -0
- package/runtime/companion/terminal-format.js +330 -0
- package/runtime/companion/vault-summary.js +169 -0
- package/runtime/companion/virtual-terminal.js +138 -0
- package/runtime/extension/shared/anthropic-backend.js +193 -0
- package/runtime/extension/shared/catalog.js +415 -0
- package/runtime/extension/shared/code-ledger.js +333 -0
- package/runtime/extension/shared/context-budget.js +259 -0
- package/runtime/extension/shared/context-summary.js +271 -0
- package/runtime/extension/shared/conversation.js +81 -0
- package/runtime/extension/shared/event-hub.js +126 -0
- package/runtime/extension/shared/extension-settings.js +445 -0
- package/runtime/extension/shared/helper-launcher.js +248 -0
- package/runtime/extension/shared/local-backend.js +706 -0
- package/runtime/extension/shared/local-server.js +149 -0
- package/runtime/extension/shared/package.json +4 -0
- package/runtime/extension/shared/practice.js +228 -0
- package/runtime/extension/shared/problem-index.js +4128 -0
- package/runtime/extension/shared/profiles.js +106 -0
- package/runtime/extension/shared/providers.js +58 -0
- package/runtime/extension/shared/reply-guard.js +864 -0
- package/runtime/extension/shared/roadmap.js +1344 -0
- package/runtime/extension/shared/sanitize.js +68 -0
- package/runtime/extension/shared/solved.js +277 -0
- package/runtime/extension/shared/tutor-engine.js +369 -0
- package/runtime/extension/shared/tutor-prompt.js +108 -0
- package/runtime/extension/shared/tutor-service.js +520 -0
- package/runtime/relay-server/package.json +4 -0
- package/runtime/relay-server/panel-broker.js +797 -0
- package/runtime/relay-server/server.js +241 -0
- package/runtime/vault-tool/log-session.js +230 -0
- package/runtime/vault-tool/package.json +4 -0
- package/runtime/vault-tool/vault-notes.js +206 -0
- package/README.md +0 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// extension/shared/sanitize.js - makes untrusted text safe to print in the terminal.
|
|
2
|
+
//
|
|
3
|
+
// Several things the companion prints come from outside this process:
|
|
4
|
+
// messages typed into the in-page panel, and every field of a capture. The
|
|
5
|
+
// relay's /capture route is CORS `*` by design (see relay-server/server.js),
|
|
6
|
+
// so ANY website the student visits can post a capture whose problemTitle or
|
|
7
|
+
// trigger carries an escape sequence. Written to a terminal unchanged, that
|
|
8
|
+
// sequence can recolor or clear the screen, retitle the window, or (on some
|
|
9
|
+
// terminals) do worse. The relay strips control characters from panel
|
|
10
|
+
// messages too, but the companion must not depend on that: it is the one
|
|
11
|
+
// writing to the terminal.
|
|
12
|
+
//
|
|
13
|
+
// The rule matches relay-server/panel-broker.js's stripControlChars: every C0
|
|
14
|
+
// control except newline and tab, DEL, and every C1 control (U+0080 to U+009F,
|
|
15
|
+
// which some terminals treat as 8-bit escape introducers, e.g. U+009B as CSI).
|
|
16
|
+
//
|
|
17
|
+
// Invisible formatting characters are dropped too, because they make text
|
|
18
|
+
// read differently from what it is (the "Trojan Source" trick): the bidi
|
|
19
|
+
// embeddings, overrides and isolates (U+202A to U+202E, U+2066 to U+2069) and
|
|
20
|
+
// the bidi marks (U+200E, U+200F, U+061C) reorder what follows them, and the
|
|
21
|
+
// zero-width space, word joiner, invisible operators and BOM (U+200B, U+2060
|
|
22
|
+
// to U+2064, U+FEFF) hide inside a word. The line and paragraph separators
|
|
23
|
+
// (U+2028, U+2029) become a plain newline, which is what they mean.
|
|
24
|
+
// Deliberately kept: the zero-width joiner and non-joiner (U+200D, U+200C).
|
|
25
|
+
// Emoji sequences (a family, a skin tone on a profession, a flag) are glued
|
|
26
|
+
// together by U+200D, and Persian and several Indic scripts need U+200C to
|
|
27
|
+
// spell words correctly; neither reorders text, so neither can disguise it.
|
|
28
|
+
|
|
29
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0b-\x1f\x7f-\x9f\u061c\u200b\u200e\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g;
|
|
30
|
+
const LINE_SEPARATORS_RE = /[\u2028\u2029]/g;
|
|
31
|
+
|
|
32
|
+
// Multi-line text (a typed message, a problem description): keeps newlines
|
|
33
|
+
// and tabs, drops every other control character.
|
|
34
|
+
export function stripControlChars(text) {
|
|
35
|
+
if (text === null || text === undefined) return '';
|
|
36
|
+
return String(text).replace(LINE_SEPARATORS_RE, '\n').replace(CONTROL_CHARS_RE, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A value shown inside a one-line label (a problem title, a slug, a trigger):
|
|
40
|
+
// control characters dropped and any run of whitespace, newlines included,
|
|
41
|
+
// collapsed to one space, so it can neither inject escapes nor break the
|
|
42
|
+
// label across lines.
|
|
43
|
+
export function oneLine(text) {
|
|
44
|
+
return stripControlChars(text).replace(/\s+/g, ' ').trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normalizes a parsed capture in place of the raw one: every string field
|
|
48
|
+
// that reaches the terminal, the panel or the model's prompt label is made
|
|
49
|
+
// safe once, here, so no call site has to remember to. The student's code is
|
|
50
|
+
// left exactly as captured (it only goes to the model and is never printed).
|
|
51
|
+
export function sanitizeCapture(capture) {
|
|
52
|
+
if (!capture || typeof capture !== 'object' || Array.isArray(capture)) return {};
|
|
53
|
+
const out = { ...capture };
|
|
54
|
+
for (const field of ['problemTitle', 'problemSlug', 'trigger', 'language']) {
|
|
55
|
+
if (out[field] === undefined || out[field] === null) continue;
|
|
56
|
+
out[field] = oneLine(out[field]) || null;
|
|
57
|
+
}
|
|
58
|
+
if (out.problemDescription !== undefined && out.problemDescription !== null) {
|
|
59
|
+
out.problemDescription = stripControlChars(out.problemDescription);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(out.problemTags)) {
|
|
62
|
+
out.problemTags = out.problemTags.filter((tag) => typeof tag === 'string').map(oneLine).filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
// The relay assigns attemptSeq, but the log file is just JSON lines: never
|
|
65
|
+
// print a non-integer.
|
|
66
|
+
if (out.attemptSeq !== undefined && !Number.isInteger(out.attemptSeq)) out.attemptSeq = null;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// extension/shared/solved.js - which problems the student has solved, kept
|
|
2
|
+
// in the extension's own storage and never sent anywhere.
|
|
3
|
+
//
|
|
4
|
+
// Storage (storage.local):
|
|
5
|
+
// solvedProblems: { [slug]: { at: ISO time, count, manual? } }
|
|
6
|
+
// at when it was last accepted (or marked by hand)
|
|
7
|
+
// count how many accepted submissions were seen for it (0 when it was
|
|
8
|
+
// only marked by hand on the Progress tab)
|
|
9
|
+
// starredProblems: { [slug]: true } the Progress tab's stars
|
|
10
|
+
// activeProgressList: list id the list the student works on (see
|
|
11
|
+
// shared/roadmap.js LISTS); missing or unknown means NeetCode 150.
|
|
12
|
+
// Changing it never touches the solved problems.
|
|
13
|
+
//
|
|
14
|
+
// Messages the background worker answers through createSolvedService:
|
|
15
|
+
// from a content script on leetcode.com/problems/<slug>/...:
|
|
16
|
+
// { type: 'lc-tutor:accepted', submissionId, slug } LeetCode accepted a
|
|
17
|
+
// submission for the problem this tab shows (`slug`, read by the
|
|
18
|
+
// content script from the live address, since sender.url does not
|
|
19
|
+
// follow in-app moves; the sender must be a problem page in a tab).
|
|
20
|
+
// Answers { ok, recorded, card }.
|
|
21
|
+
// from the extension's own pages (the Progress tab):
|
|
22
|
+
// { type: 'lc-tutor:solved-set', slug, solved } mark or unmark by hand
|
|
23
|
+
// { type: 'lc-tutor:star-set', slug, starred }
|
|
24
|
+
// from either (the Progress tab's list selector, the card's list menu):
|
|
25
|
+
// { type: 'lc-tutor:set-list', list, slug? } work on another list. From
|
|
26
|
+
// a problem page (with the card's `slug`, as for lc-tutor:accepted)
|
|
27
|
+
// the answer carries that card again, built for the new list:
|
|
28
|
+
// { ok, card }.
|
|
29
|
+
// from a content script on a problem page (the Tutor panel's "Practice a
|
|
30
|
+
// skill" picker, with the page's `slug` as for lc-tutor:accepted):
|
|
31
|
+
// { type: 'lc-tutor:practice', slug, skill?, offset?, skip? } read only. The
|
|
32
|
+
// reference difficulty is that problem's in the bundled index (none
|
|
33
|
+
// for a problem it lacks), and that problem is never suggested.
|
|
34
|
+
// Answers { ok, skills, skill, reference, suggestions, offset, more }
|
|
35
|
+
// (see shared/practice.js); without `skill` only the list of skills
|
|
36
|
+
// matters. Nothing of it reaches the tutor.
|
|
37
|
+
//
|
|
38
|
+
// Pure JavaScript with no Node API (extension/shared.test.js checks that).
|
|
39
|
+
|
|
40
|
+
import { catalogEntry } from './catalog.js';
|
|
41
|
+
import { practiceSuggestions, referenceLevel, skillById, skillChoices } from './practice.js';
|
|
42
|
+
import { LISTS, NEETCODE_150, ROADMAPS, listById, problemInList, suggestAfterSolve } from './roadmap.js';
|
|
43
|
+
|
|
44
|
+
export const SOLVED_KEY = 'solvedProblems';
|
|
45
|
+
export const STARRED_KEY = 'starredProblems';
|
|
46
|
+
export const ACTIVE_LIST_KEY = 'activeProgressList';
|
|
47
|
+
|
|
48
|
+
export function isListId(value) {
|
|
49
|
+
return typeof value === 'string' && Object.hasOwn(ROADMAPS, value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// LeetCode's own slugs: lowercase letters and digits joined by single
|
|
53
|
+
// hyphens ("two-sum", "3sum", "powx-n").
|
|
54
|
+
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
55
|
+
const MAX_SLUG = 120;
|
|
56
|
+
|
|
57
|
+
export function isSlug(value) {
|
|
58
|
+
return typeof value === 'string' && value.length <= MAX_SLUG && SLUG_RE.test(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The problem slug of a leetcode.com problem page address, or null.
|
|
62
|
+
export function slugFromUrl(url) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(url);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'leetcode.com') return null;
|
|
70
|
+
const match = /^\/problems\/([^/]+)/.exec(parsed.pathname);
|
|
71
|
+
return match && isSlug(match[1]) ? match[1] : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validTime(value) {
|
|
75
|
+
return typeof value === 'string' && !Number.isNaN(Date.parse(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Whatever storage held, as a clean record: unknown keys and malformed
|
|
79
|
+
// entries are dropped.
|
|
80
|
+
export function normalizeSolved(raw) {
|
|
81
|
+
const out = {};
|
|
82
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
83
|
+
for (const [slug, entry] of Object.entries(raw)) {
|
|
84
|
+
if (!isSlug(slug) || !entry || typeof entry !== 'object') continue;
|
|
85
|
+
const count = Number.isInteger(entry.count) && entry.count >= 0 ? entry.count : 0;
|
|
86
|
+
const clean = { at: validTime(entry.at) ? entry.at : new Date(0).toISOString(), count };
|
|
87
|
+
if (entry.manual === true) clean.manual = true;
|
|
88
|
+
out[slug] = clean;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeStarred(raw) {
|
|
94
|
+
const out = {};
|
|
95
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
96
|
+
for (const [slug, on] of Object.entries(raw)) if (isSlug(slug) && on === true) out[slug] = true;
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An accepted submission: the count goes up and `at` moves to now. A problem
|
|
101
|
+
// marked by hand before counts as detected from now on.
|
|
102
|
+
export function recordAccepted(solved, slug, at) {
|
|
103
|
+
const before = solved[slug];
|
|
104
|
+
return { ...solved, [slug]: { at, count: (before ? before.count : 0) + 1 } };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The Progress tab's status mark. Marking keeps a detected record as it is;
|
|
108
|
+
// unmarking removes the problem whatever made it solved.
|
|
109
|
+
export function markSolved(solved, slug, on, at) {
|
|
110
|
+
const next = { ...solved };
|
|
111
|
+
if (!on) delete next[slug];
|
|
112
|
+
else if (!next[slug]) next[slug] = { at, count: 0, manual: true };
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function solvedSlugs(solved) {
|
|
117
|
+
return Object.keys(solved);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function brief(problem) {
|
|
121
|
+
return problem ? { slug: problem.slug, title: problem.title, difficulty: problem.difficulty } : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// What the Tutor panel's "nice job" card shows after `slug` was accepted,
|
|
125
|
+
// built only from the active list, the catalog and the solved list:
|
|
126
|
+
// { roadmap, lists, solvedTitle, next,
|
|
127
|
+
// more: { topic, label, problem } | null, finished, skills }
|
|
128
|
+
// `lists` is every list the card's menu offers ({ id, title, size }), and
|
|
129
|
+
// `skills` the "Practice a skill" chips ({ id, label }, a fixed list, never
|
|
130
|
+
// the solved problem's own tags).
|
|
131
|
+
// `solvedTitle` is the catalog's title, or null for a problem no bundled
|
|
132
|
+
// list has (the panel then uses the page's own title). `more` is null for a
|
|
133
|
+
// problem the active list does not have (then the card is the agenda only),
|
|
134
|
+
// or when its topic has nothing left that `next` does not already suggest.
|
|
135
|
+
export function congratulation(solved, slug, roadmap = NEETCODE_150) {
|
|
136
|
+
const done = solvedSlugs(solved);
|
|
137
|
+
const suggestion = suggestAfterSolve(roadmap, done, slug, { count: 2 });
|
|
138
|
+
const next = suggestion.agenda[0] || null;
|
|
139
|
+
const sameTopic = suggestion.sameTopic;
|
|
140
|
+
const other = sameTopic.problems.find((p) => !next || p.slug !== next.slug) || null;
|
|
141
|
+
const entry = problemInList(roadmap, slug) || catalogEntry(slug);
|
|
142
|
+
const label = sameTopic.topic && `More ${sameTopic.topic.title} problems`;
|
|
143
|
+
return {
|
|
144
|
+
roadmap: suggestion.roadmap,
|
|
145
|
+
lists: LISTS.map((list) => ({ id: list.id, title: list.title, size: list.size })),
|
|
146
|
+
solvedTitle: entry ? entry.title : null,
|
|
147
|
+
next: brief(next),
|
|
148
|
+
more: sameTopic.topic && other ? { topic: sameTopic.topic, label, problem: brief(other) } : null,
|
|
149
|
+
finished: !next,
|
|
150
|
+
skills: skillChoices(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const MAX_OFFSET = 5000;
|
|
155
|
+
|
|
156
|
+
// The "Practice a skill" answer for the problem page `slug`: the skills,
|
|
157
|
+
// and with a known `skill` the suggestions after `offset`.
|
|
158
|
+
export function practiceAnswer(solved, slug, skill, offset = 0, skip = []) {
|
|
159
|
+
const result = practiceSuggestions({ skill: skillById(skill), reference: referenceLevel(slug), solved, exclude: [slug, ...skip], offset });
|
|
160
|
+
return { ok: true, skills: skillChoices(), ...result };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The background worker's side. `storage` is { get(keys), set(items) };
|
|
164
|
+
// every write goes through one queue so two quick events never lose one
|
|
165
|
+
// another's change.
|
|
166
|
+
export function createSolvedService({ storage, extensionOrigin, now = () => new Date().toISOString(), log = () => {} }) {
|
|
167
|
+
let chain = Promise.resolve();
|
|
168
|
+
function update(fn) {
|
|
169
|
+
const run = chain.then(async () => {
|
|
170
|
+
const stored = (await storage.get([SOLVED_KEY, STARRED_KEY, ACTIVE_LIST_KEY])) || {};
|
|
171
|
+
return fn(normalizeSolved(stored[SOLVED_KEY]), normalizeStarred(stored[STARRED_KEY]), listById(stored[ACTIVE_LIST_KEY]));
|
|
172
|
+
});
|
|
173
|
+
chain = run.catch(() => {});
|
|
174
|
+
return run;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The same submission is reported once: LeetCode may poll its result
|
|
178
|
+
// again, a page may be reloaded mid-poll, and a worker may restart.
|
|
179
|
+
const seen = [];
|
|
180
|
+
const MAX_SEEN = 200;
|
|
181
|
+
|
|
182
|
+
function isExtensionPage(sender) {
|
|
183
|
+
return Boolean(sender && typeof sender.url === 'string' && sender.url.startsWith(`${extensionOrigin}/`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The problem a message from a problem tab is about. The sender must be a
|
|
187
|
+
// leetcode.com problem page in a tab, but its address (sender.url) is where
|
|
188
|
+
// the document was loaded: LeetCode moves between problems in the page
|
|
189
|
+
// (history.pushState) and sender.url does not follow, so the content
|
|
190
|
+
// script names the problem from the page's live address.
|
|
191
|
+
function tabSlug(message, sender) {
|
|
192
|
+
if (!sender || !sender.tab || !slugFromUrl(sender.url)) return null;
|
|
193
|
+
return isSlug(message.slug) ? message.slug : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// content.js sends this only when the submission's first check started on
|
|
197
|
+
// the problem the page shows now (see inject.js), so a verdict that arrives
|
|
198
|
+
// after a move never marks the next problem solved.
|
|
199
|
+
async function accepted(message, sender) {
|
|
200
|
+
const slug = tabSlug(message, sender);
|
|
201
|
+
const id = message.submissionId;
|
|
202
|
+
if (!slug || typeof id !== 'string' || !/^\d{1,20}$/.test(id)) return { ok: false };
|
|
203
|
+
const key = `${slug}:${id}`;
|
|
204
|
+
if (seen.includes(key)) return { ok: true, recorded: false, card: null };
|
|
205
|
+
seen.push(key);
|
|
206
|
+
if (seen.length > MAX_SEEN) seen.shift();
|
|
207
|
+
return update(async (solved, _starred, list) => {
|
|
208
|
+
const next = recordAccepted(solved, slug, now());
|
|
209
|
+
await storage.set({ [SOLVED_KEY]: next });
|
|
210
|
+
return { ok: true, recorded: true, card: congratulation(next, slug, list) };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Only the list choice is written; the solved problems stay as they are.
|
|
215
|
+
async function setList(message, sender) {
|
|
216
|
+
const page = isExtensionPage(sender);
|
|
217
|
+
const slug = page ? null : tabSlug(message, sender);
|
|
218
|
+
if (!isListId(message.list) || (!page && !slug)) return { ok: false };
|
|
219
|
+
return update(async (solved) => {
|
|
220
|
+
await storage.set({ [ACTIVE_LIST_KEY]: message.list });
|
|
221
|
+
return slug ? { ok: true, card: congratulation(solved, slug, listById(message.list)) } : { ok: true };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Read only: the solved problems as stored now (after any queued write).
|
|
226
|
+
async function practice(message, sender) {
|
|
227
|
+
const slug = tabSlug(message, sender);
|
|
228
|
+
const offset = message.offset === undefined ? 0 : message.offset;
|
|
229
|
+
const skill = message.skill === undefined || message.skill === null ? null : message.skill;
|
|
230
|
+
if (!slug || !Number.isInteger(offset) || offset < 0 || offset > MAX_OFFSET) return { ok: false };
|
|
231
|
+
if (skill !== null && !skillById(skill)) return { ok: false };
|
|
232
|
+
// Problems the asking card already shows, left out so none appears twice.
|
|
233
|
+
const skip = message.skip === undefined ? [] : message.skip;
|
|
234
|
+
if (!Array.isArray(skip) || skip.length > 4 || !skip.every(isSlug)) return { ok: false };
|
|
235
|
+
return update(async (solved) => practiceAnswer(solved, slug, skill, offset, skip));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function setSolved(message) {
|
|
239
|
+
if (!isSlug(message.slug) || typeof message.solved !== 'boolean') return { ok: false };
|
|
240
|
+
return update(async (solved) => {
|
|
241
|
+
await storage.set({ [SOLVED_KEY]: markSolved(solved, message.slug, message.solved, now()) });
|
|
242
|
+
return { ok: true };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function setStarred(message) {
|
|
247
|
+
if (!isSlug(message.slug) || typeof message.starred !== 'boolean') return { ok: false };
|
|
248
|
+
return update(async (_solved, starred) => {
|
|
249
|
+
const next = { ...starred };
|
|
250
|
+
if (message.starred) next[message.slug] = true;
|
|
251
|
+
else delete next[message.slug];
|
|
252
|
+
await storage.set({ [STARRED_KEY]: next });
|
|
253
|
+
return { ok: true };
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
// Resolves to the answer, or undefined when the message is not for this
|
|
259
|
+
// service.
|
|
260
|
+
async handleMessage(message, sender) {
|
|
261
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
262
|
+
try {
|
|
263
|
+
if (message.type === 'lc-tutor:accepted') return await accepted(message, sender);
|
|
264
|
+
if (message.type === 'lc-tutor:set-list') return await setList(message, sender);
|
|
265
|
+
if (message.type === 'lc-tutor:practice') return await practice(message, sender);
|
|
266
|
+
if (message.type === 'lc-tutor:solved-set' || message.type === 'lc-tutor:star-set') {
|
|
267
|
+
if (!isExtensionPage(sender)) return { ok: false };
|
|
268
|
+
return message.type === 'lc-tutor:solved-set' ? await setSolved(message) : await setStarred(message);
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
log(`could not update solved problems: ${err && err.message}`);
|
|
272
|
+
return { ok: false };
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|