feat: implement robust student ID discovery and multi-endpoint data extraction for activities
CI Verification / Lint & Extension Integrity Check (push) Successful in 18s
CI Verification / Lint & Extension Integrity Check (push) Successful in 18s
This commit is contained in:
+267
-25
@@ -6,13 +6,55 @@
|
||||
(function () {
|
||||
console.log('[Brightwheel Exporter] Content script initialized.');
|
||||
|
||||
// Extract student ID from URL if present
|
||||
// Extract student ID from URL, DOM, or Storage
|
||||
function detectCurrentStudentId() {
|
||||
const match = window.location.pathname.match(/\/students\/([a-zA-Z0-9-]+)/);
|
||||
if (match && match[1]) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: match[1] });
|
||||
return match[1];
|
||||
// 1. Check URL pathname & search params
|
||||
const pathMatch = window.location.pathname.match(/\/(?:students|children|profiles)\/([a-zA-Z0-9-]+)/);
|
||||
if (pathMatch && pathMatch[1]) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: pathMatch[1] });
|
||||
return pathMatch[1];
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paramId = urlParams.get('student_id') || urlParams.get('studentId') || urlParams.get('child_id');
|
||||
if (paramId) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: paramId });
|
||||
return paramId;
|
||||
}
|
||||
|
||||
// 2. Check DOM links or attributes
|
||||
const studentLink = document.querySelector('a[href*="/students/"], a[href*="/children/"], [data-student-id]');
|
||||
if (studentLink) {
|
||||
const href = studentLink.getAttribute('href') || '';
|
||||
const hrefMatch = href.match(/\/(?:students|children)\/([a-zA-Z0-9-]+)/);
|
||||
if (hrefMatch && hrefMatch[1]) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: hrefMatch[1] });
|
||||
return hrefMatch[1];
|
||||
}
|
||||
const dataId = studentLink.getAttribute('data-student-id');
|
||||
if (dataId) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: dataId });
|
||||
return dataId;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check localStorage for student UUID
|
||||
try {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
const val = localStorage.getItem(key);
|
||||
if (typeof val === 'string' && (key.includes('student') || key.includes('child') || key.includes('user') || key.includes('auth'))) {
|
||||
const uuidMatch = val.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
||||
if (uuidMatch && uuidMatch[0]) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: uuidMatch[0] });
|
||||
return uuidMatch[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Storage access blocked or restricted
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,6 +70,118 @@
|
||||
}
|
||||
}).observe(document, { subtree: true, childList: true });
|
||||
|
||||
// Deep extract student profiles from API payloads
|
||||
function extractStudentsFromData(data) {
|
||||
if (!data) return [];
|
||||
const students = [];
|
||||
const seenIds = new Set();
|
||||
|
||||
function addStudent(s) {
|
||||
if (!s) return;
|
||||
const sId = s.id || s.object_id || s.student_id;
|
||||
if (sId && !seenIds.has(sId)) {
|
||||
seenIds.add(sId);
|
||||
students.push({
|
||||
id: sId,
|
||||
first_name: s.first_name || s.name?.split(' ')?.[0] || 'Child',
|
||||
last_name: s.last_name || s.name?.split(' ')?.slice(1).join(' ') || '',
|
||||
picture_url: s.picture_url || s.avatar_url || s.photo_url || s.profile_photo?.image_url || s.profile_photo?.thumbnail_url || null,
|
||||
school_name: s.school_name || s.school?.name || s.room_name || s.room?.name || 'Brightwheel Profile',
|
||||
room: s.room || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(data.students)) data.students.forEach(addStudent);
|
||||
if (Array.isArray(data.children)) data.children.forEach(addStudent);
|
||||
if (Array.isArray(data.profiles)) data.profiles.forEach(addStudent);
|
||||
if (Array.isArray(data.guardians)) {
|
||||
data.guardians.forEach(g => {
|
||||
if (Array.isArray(g.students)) g.students.forEach(addStudent);
|
||||
if (Array.isArray(g.children)) g.children.forEach(addStudent);
|
||||
});
|
||||
}
|
||||
if (data.student) addStudent(data.student);
|
||||
if (data.user?.students) data.user.students.forEach(addStudent);
|
||||
|
||||
if (Array.isArray(data.activities) || Array.isArray(data)) {
|
||||
const items = Array.isArray(data.activities) ? data.activities : data;
|
||||
for (const item of items) {
|
||||
if (item.target && item.target.user_type === 'student') {
|
||||
addStudent(item.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return students;
|
||||
}
|
||||
|
||||
// Fetch full student & session discovery directly within authenticated tab
|
||||
async function discoverSessionAndStudents() {
|
||||
const results = {
|
||||
authenticated: false,
|
||||
user: null,
|
||||
students: [],
|
||||
activeStudentId: detectCurrentStudentId(),
|
||||
liveActivitiesSample: []
|
||||
};
|
||||
|
||||
try {
|
||||
const meRes = await fetch('https://schools.mybrightwheel.com/api/v1/users/me', {
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
|
||||
if (meRes) {
|
||||
results.authenticated = true;
|
||||
results.user = meRes;
|
||||
const found = extractStudentsFromData(meRes);
|
||||
found.forEach(s => results.students.push(s));
|
||||
}
|
||||
|
||||
const guardianRes = await fetch('https://schools.mybrightwheel.com/api/v1/guardians/me', {
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
|
||||
if (guardianRes) {
|
||||
results.authenticated = true;
|
||||
const found = extractStudentsFromData(guardianRes);
|
||||
found.forEach(s => {
|
||||
if (!results.students.some(existing => existing.id === s.id)) {
|
||||
results.students.push(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const actProbe = await fetch('https://schools.mybrightwheel.com/api/v1/activities?page_size=10', {
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
|
||||
if (actProbe) {
|
||||
results.authenticated = true;
|
||||
const acts = actProbe.activities || actProbe.items || [];
|
||||
results.liveActivitiesSample = acts.slice(0, 5);
|
||||
const fromActs = extractStudentsFromData(acts);
|
||||
fromActs.forEach(s => {
|
||||
if (!results.students.some(existing => existing.id === s.id)) {
|
||||
results.students.push(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (results.students.length > 0 && !results.activeStudentId) {
|
||||
results.activeStudentId = results.students[0].id;
|
||||
chrome.storage.local.set({ lastActiveStudentId: results.activeStudentId });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Brightwheel Exporter] Discovery note:', e);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Listen for messages from extension popup or dashboard
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'GET_PAGE_CONTEXT') {
|
||||
@@ -40,49 +194,137 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'GET_STUDENTS_AND_SESSION') {
|
||||
discoverSessionAndStudents().then(res => {
|
||||
sendResponse({ success: true, ...res });
|
||||
}).catch(err => {
|
||||
sendResponse({ success: false, error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'SCRAPE_DOM_FEED') {
|
||||
const cards = scrapeDomFeedCards();
|
||||
sendResponse({
|
||||
success: true,
|
||||
activities: cards,
|
||||
count: cards.length
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'INSPECT_LIVE_FEED') {
|
||||
(async () => {
|
||||
try {
|
||||
const studentId = message.studentId || detectCurrentStudentId();
|
||||
const apiListRes = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`, {
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
||||
const endpointProbes = {};
|
||||
|
||||
const rawActs = apiListRes?.activities || apiListRes?.items || [];
|
||||
|
||||
// Sample detail fetches for video, nap, checkin
|
||||
const sampleDetails = {};
|
||||
for (const act of rawActs.slice(0, 10)) {
|
||||
const type = act.activity_type || act.action_type || act.type;
|
||||
if (!sampleDetails[type] && act.id) {
|
||||
const detail = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities/${act.id}`, {
|
||||
async function probe(name, url) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
||||
sampleDetails[type] = detail;
|
||||
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const json = await res.json().catch(() => null);
|
||||
const items = json?.activities || json?.items || (Array.isArray(json) ? json : []);
|
||||
endpointProbes[name] = {
|
||||
url,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
itemCount: items.length,
|
||||
latestDate: items[0]?.event_date || items[0]?.created_at || null,
|
||||
earliestDate: items[items.length - 1]?.event_date || items[items.length - 1]?.created_at || null,
|
||||
sampleItems: items.slice(0, 5),
|
||||
rawSample: json
|
||||
};
|
||||
} catch (e) {
|
||||
endpointProbes[name] = { url, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Probe general activities endpoint
|
||||
await probe('parent_activities_feed', 'https://schools.mybrightwheel.com/api/v1/activities?page_size=25');
|
||||
|
||||
// 2. Probe student-specific endpoint
|
||||
if (studentId) {
|
||||
await probe('student_activities_feed', `https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`);
|
||||
}
|
||||
|
||||
// 3. Probe user/guardian profiles
|
||||
await probe('users_me', 'https://schools.mybrightwheel.com/api/v1/users/me');
|
||||
await probe('guardians_me', 'https://schools.mybrightwheel.com/api/v1/guardians/me');
|
||||
|
||||
// 4. Probe feed / timeline
|
||||
await probe('feed_endpoint', 'https://schools.mybrightwheel.com/api/v1/feed?page_size=25');
|
||||
|
||||
const domCards = scrapeDomFeedCards();
|
||||
const reactFiberActivities = extractActivitiesFromReactFiber();
|
||||
|
||||
sendResponse({
|
||||
success: true,
|
||||
studentId,
|
||||
apiSampleActivities: rawActs.slice(0, 10),
|
||||
sampleDetails,
|
||||
domCards: domCards.slice(0, 20),
|
||||
currentUrl: window.location.href,
|
||||
pageTitle: document.title,
|
||||
currentUrl: window.location.href
|
||||
endpointProbes,
|
||||
domCards: domCards.slice(0, 20),
|
||||
reactFiberCount: reactFiberActivities.length,
|
||||
reactFiberSample: reactFiberActivities.slice(0, 5)
|
||||
});
|
||||
} catch (err) {
|
||||
sendResponse({ success: false, error: err.message });
|
||||
}
|
||||
})();
|
||||
return true; // async
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Deep React Fiber state extractor for rendered activities
|
||||
function extractActivitiesFromReactFiber() {
|
||||
const rawItems = [];
|
||||
const seenIds = new Set();
|
||||
|
||||
function inspectProps(props) {
|
||||
if (!props || typeof props !== 'object') return;
|
||||
const candidate = props.activity || props.item || props.feedItem || props.record || props.event;
|
||||
if (candidate && typeof candidate === 'object') {
|
||||
const id = candidate.object_id || candidate.id;
|
||||
if (id && !seenIds.has(id)) {
|
||||
seenIds.add(id);
|
||||
rawItems.push(candidate);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(props.activities)) {
|
||||
props.activities.forEach(a => {
|
||||
const id = a?.object_id || a?.id;
|
||||
if (id && !seenIds.has(id)) {
|
||||
seenIds.add(id);
|
||||
rawItems.push(a);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
document.querySelectorAll('*').forEach(node => {
|
||||
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
|
||||
if (fiberKey && node[fiberKey]) {
|
||||
let fiber = node[fiberKey];
|
||||
let depth = 0;
|
||||
while (fiber && depth < 10) {
|
||||
inspectProps(fiber.memoizedProps);
|
||||
inspectProps(fiber.pendingProps);
|
||||
fiber = fiber.return;
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return rawItems;
|
||||
}
|
||||
|
||||
// DOM Scraper for Brightwheel Web Feed
|
||||
function scrapeDomFeedCards() {
|
||||
const results = [];
|
||||
|
||||
Reference in New Issue
Block a user