Refactor Google Meet Transcripts Extension for Local Use

- Removed all cloud-related functionalities, including login prompts and token handling.
- Disabled Laxis cloud features, ensuring no data is sent to external servers.
- Updated manifest to reflect the new local-only functionality.
- Added a new Python server to handle transcripts locally, including WebSocket support.
- Implemented storage management for transcripts, including deduplication and file writing.
- Created a smoke test for the WebSocket server to simulate transcript updates.
- Updated README with setup instructions and usage details for the new local server.
This commit is contained in:
vahidaskari
2026-06-12 00:31:32 +03:30
parent 602dcb7430
commit 7bc34c79ed
35 changed files with 1069 additions and 840 deletions
@@ -1 +1,4 @@
//!function(e,t,c,r,n,s){e.GoogleAnalyticsObject=r,e[r]=e[r]||function(){(e[r].q=e[r].q||[]).push(arguments)},e[r].l=1*new Date,n=t.createElement(c),s=t.getElementsByTagName(c)[0],n.async=1,n.src=chrome.runtime.getURL("analytics/analyticsSource.js"),n.id="laxis-track",s.parentNode.insertBefore(n,s)}(window,document,"script","ga");let firstScript=document.getElementsByTagName("script")[1],s=document.createElement("script");s.src=chrome.runtime.getURL("analytics/load.js"),firstScript.parentNode.insertBefore(s,firstScript);
// analytics.js — غیرفعال شد.
// در نسخه‌ی اصلی laxis این فایل Google Analytics (laxis-track) را تزریق می‌کرد
// و به analytics/analyticsSource.js و analytics/load.js وابسته بود (که دیگر وجود ندارند).
// برای حذف ردیابی و ارسال داده، محتوای آن کاملاً برداشته شد. این فایل در manifest هم لود نمی‌شود.
@@ -0,0 +1,68 @@
// bridge.js — پل ارتباطی افزونه با سرور لوکال پایتون (MVP)
// این فایل داخل service worker اجرا می‌شود (importScripts از login.js).
// content script هر caption را با پیام {type:"TRANSCRIPT_UPDATE"} می‌فرستد،
// و این‌جا روی WebSocket به سرور پایتون (ws://127.0.0.1:8765) forward می‌شود.
//
// چرا از service worker و نه content script؟ چون content script تابعِ CSP سختگیر
// google meet است و connect-src اتصال ws را بلاک می‌کند؛ service worker این محدودیت را ندارد.
const BRIDGE_URL = "ws://127.0.0.1:8765";
let ws = null;
const pending = []; // پیام‌هایی که موقع وصل‌نبودن سوکت صف می‌شوند (MVP: کوتاه)
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
return;
}
ws = new WebSocket(BRIDGE_URL);
ws.onopen = () => {
console.log("[bridge] connected to", BRIDGE_URL);
while (pending.length && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(pending.shift()));
}
};
ws.onmessage = (event) => {
// پاسخ سرور پایتون (ACK / PONG) — فعلاً فقط لاگ
console.log("[bridge] <-", event.data);
};
ws.onclose = () => {
console.log("[bridge] disconnected, retry in 3s");
ws = null;
setTimeout(connect, 3000);
};
ws.onerror = () => {
try { ws && ws.close(); } catch (e) {}
};
}
function sendToBridge(payload) {
connect();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
} else {
pending.push(payload);
if (pending.length > 500) pending.splice(0, pending.length - 500);
}
}
// heartbeat: از Chrome 116+ تبادل پیامِ کمتر از ۳۰ ثانیه، service worker را زنده نگه می‌دارد
setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "PING", ts: Date.now() }));
}
}, 20000);
// دریافت caption از content script و forward به سرور پایتون
chrome.runtime.onMessage.addListener((message) => {
if (message && message.type === "TRANSCRIPT_UPDATE") {
sendToBridge(message);
}
});
connect();
@@ -1 +1 @@
const domainUrl="https://app.laxis.tech",extensionId=chrome.runtime.id,loginUrl=domainUrl+"/login?source=chrome-extension&extensionId="+extensionId,upgradeUrl=domainUrl+"/settings/plan",signupUrl=domainUrl+"/signup?source=chrome-extension&extensionId="+extensionId,googleMeetUrl="https://meet.google.com/*";
const domainUrl="https://example.com",extensionId=chrome.runtime.id,loginUrl=domainUrl+"/login?source=chrome-extension&extensionId="+extensionId,upgradeUrl=domainUrl+"/settings/plan",signupUrl=domainUrl+"/signup?source=chrome-extension&extensionId="+extensionId,googleMeetUrl="https://meet.google.com/*";
@@ -12,8 +12,8 @@ const addTutorialPanel = () => {
t.style.display = "flex", t.style.alignItems = "center", t.style.paddingLeft = "8px";
const i = document.createElement("img");
i.src = chrome.runtime.getURL("image/logo.png"), i.addEventListener("click", (function() {
window.open(`${domainUrl}/login`)
})), i.alt = "none", i.style.height = "30px", i.style.width = "90px", i.style.marginLeft = "-16px", i.style.cursor = "pointer", i.style.zIndex = "999", t.appendChild(i), e.appendChild(t);
void 0 /* لینک لاگین laxis حذف شد */
})), i.alt = "none", i.style.height = "30px", i.style.width = "90px", i.style.marginLeft = "0px", i.style.cursor = "pointer", i.style.zIndex = "999", t.appendChild(i), e.appendChild(t);
const o = document.createElement("div");
o.style.display = "flex", o.style.alignItems = "center";
const d = document.createElement("div");
@@ -42,7 +42,7 @@ const addTutorialPanel = () => {
const g = createCaptionOnIcon();
y.appendChild(g), u.appendChild(y), s.appendChild(u);
let h = document.createElement("div");
h.style.textAlign = "center", h.innerHTML = "Laxis supports 69 languages in Google Meet", s.appendChild(h);
h.style.textAlign = "center", h.innerHTML = "Supports 69 languages in Google Meet", s.appendChild(h);
let x = document.createElement("div");
x.style.paddingTop = "32px", x.innerHTML = "Download", s.appendChild(x);
let C = document.createElement("div");
@@ -52,7 +52,7 @@ const addTutorialPanel = () => {
let E = createDownloadIcon();
v.appendChild(E), C.appendChild(v), s.appendChild(C);
let b = document.createElement("div");
b.style.textAlign = "center", b.innerHTML = "Laxis provides multiple formats for downloading the transcripts.", s.appendChild(b), l.appendChild(s), n.appendChild(l), dragElement(n)
b.style.textAlign = "center", b.innerHTML = "Download transcripts in multiple formats.", s.appendChild(b), l.appendChild(s), n.appendChild(l), dragElement(n)
}
};
@@ -68,9 +68,7 @@ function addMiniPanel() {
const n = document.createElement("div");
n.setAttribute("name", "laxis-rootHeader"), n.style.cursor = "move", n.classList.add("col-12"), n.style.height = "50px", n.style.width = "50px", n.style.marginBottom = "-10px", n.style.cursor = "move", n.style.zIndex = "999";
const l = document.createElement("img");
l.src = chrome.runtime.getURL("image/logo.png"), l.addEventListener("click", (function() {
window.open(`${domainUrl}/login`)
})), l.style.cursor = "pointer", l.style.marginTop = "10px", l.style.height = "25px", l.style.width = "75px", l.style.marginLeft = "-16px", l.alt = "none", n.appendChild(l), t.appendChild(n);
l.src = chrome.runtime.getURL("image/logo128x128.png"), l.style.cursor = "pointer", l.style.marginTop = "5px", l.style.height = "40px", l.style.width = "40px", l.style.marginLeft = "5px", l.alt = "none", n.appendChild(l), t.appendChild(n);
const i = document.createElement("div");
i.title = "Expand", i.id = "laxis-expandPanel", i.classList.add("miniButtonContainer"), i.addEventListener("click", addTutorialPanel);
const o = createExpandIcon();
@@ -112,8 +110,8 @@ const addCaptionPanel = () => {
p.style.display = "flex", p.style.alignItems = "center", p.style.paddingLeft = "8px";
const m = document.createElement("img");
m.src = chrome.runtime.getURL("image/logo.png"), m.alt = "none", m.addEventListener("click", (function() {
window.open(`${domainUrl}/login`)
})), m.style.height = "30px", m.style.marginLeft = "-16px", m.style.cursor = "pointer", m.style.zIndex = "999", p.appendChild(m), r.appendChild(p);
void 0 /* لینک لاگین laxis حذف شد */
})), m.style.height = "30px", m.style.marginLeft = "0px", m.style.cursor = "pointer", m.style.zIndex = "999", p.appendChild(m), r.appendChild(p);
const u = document.createElement("div");
u.style.display = "flex", u.style.alignItems = "center";
const y = document.createElement("div");
@@ -144,7 +142,6 @@ const addCaptionPanel = () => {
const I = document.createElement("div");
I.style.width = "300px", I.style.backgroundColor = "#292c35", I.style.paddingTop = "8px";
const w = document.createElement("div");
w.id = "login-prompt", w.style.display = "none", w.style.margin = "0px 8px", w.style.padding = "4px", w.style.textAlign = "center", w.addEventListener("click", signup), w.style.cursor = "pointer", w.innerHTML = "Please <span style='color:#2196f3'>log in</span> to enable autosave to Laxis Cloud.", w.style.borderRadius = "12px", w.style.border = "1px solid rgba(255, 255, 255, 0.5)", w.style.backgroundColor = "#454953", w.style.color = "#ffffff", w.style.fontSize = "10px";
const T = document.createElement("div");
T.id = "google-meet-quota", T.style.display = "none", T.style.margin = "0px 8px", T.style.padding = "4px", T.style.textAlign = "center", T.addEventListener("click", upgrade), T.style.cursor = "pointer", T.innerHTML = "Used Google Meet Quota: ", T.style.borderRadius = "12px", T.style.border = "1px solid rgba(255, 255, 255, 0.5)", T.style.backgroundColor = "#454953", T.style.color = "#ffffff", T.style.fontSize = "10px";
const M = document.createElement("div");
@@ -192,13 +189,11 @@ const addCaptionPanel = () => {
n.className = "modal-header", n.innerHTML = "Download", t.appendChild(n);
let l = document.createElement("div"),
i = document.createElement("div");
i.className = "modal-body", i.innerHTML = "File Type<select name='extension' id='extension' value='txt'><option value='txt'>txt</option><option value='app'>Laxis Cloud</option><option value='doc'>doc</option><option value='pdf'>pdf</option></select>", l.appendChild(i);
let o = document.createElement("div");
o.className = "modal-body", o.innerHTML = "<div>Highlights</div><div><input type='checkbox' name='highlightCheck' id='highlightCheck' checked='true'/></div>", l.appendChild(o);
let d = document.createElement("div");
d.className = "modal-body", d.innerHTML = "<div>Timestamps</div><div><input type='checkbox' name='timestampCheck' id='timestampCheck'/></div>", l.appendChild(d);
let a = document.createElement("div");
a.className = "modal-body", a.title = "Autosave to Laxis every 5 minutes", a.style.cursor = "help", a.innerHTML = "<div>Autosave to Laxis Cloud</div><div><input type='checkbox' name='autoSaveCheck' id='autoSaveCheck'/></div>", l.appendChild(a), t.appendChild(l);
let s = document.createElement("div");
s.className = "modal-footer";
let c = document.createElement("button");
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
const makeFullKey=e=>`__laxis_${e}`,makeTranscriptKey=(...e)=>{const[t,r,o]=e,n=[`hangout_${t}`];return e.length>=2&&(n.push(`session_${r}`),e.length>=3&&n.push(`speaker_${o}`)),n.join("_")},get=e=>{const t=window.localStorage.getItem(makeFullKey(e));return"string"==typeof t||t instanceof String?(debug(e,t),JSON.parse(t)):t},set=(e,t,r=!1)=>{const o=makeFullKey(e),n=JSON.stringify(t),a=makeFullKey("rotationKeys");let s=JSON.parse(window.localStorage.getItem(a))||[],i=new Set(s);for(;;)try{window.localStorage.setItem(o,n),r&&(i.add(o),window.localStorage.setItem(a,JSON.stringify(Array.from(i))));break}catch(e){if(e instanceof DOMException&&("QuotaExceededError"===e.name||e.code===DOMException.QUOTA_EXCEEDED_ERR)){if(console.log("Local storage quota exceeded! Deleting old keys to make room for new ones. This may take a while..."),debug("Local storage quota exceeded! Deleting old keys to make room for new ones. This may take a while..."),i.size>0){for(let e=0;e<5&&i.size>0;e++){const e=i.values().next().value;i.delete(e),window.localStorage.removeItem(e)}window.localStorage.setItem(a,JSON.stringify(Array.from(i)));continue}console.error("No keys available to delete.");break}console.error("Unexpected error:",e);break}},remove=e=>{debug(`remove ${makeFullKey(e)}`),window.localStorage.removeItem(makeFullKey(e))},getOrSet=(e,t)=>{const r=get(e);return null==r?(set(e,t),t):r},increment=e=>{const t=get(e);if(null==t)return set(e,0),0;{let r=t+1;return set(e,r),r}},setSpeaker=e=>{set(makeTranscriptKey(e.transcriptId,e.sessionIndex,e.speakerIndex),{image:e.image,person:e.person,text:e.text,startedAt:e.startedAt,endedAt:e.endedAt,highlight:e.highlight},!0)},getTranscript=e=>{const t=get(makeTranscriptKey(e))||0;let r=[];const o=get(makeTranscriptKey(e,t))||0;for(let n=0;n<=o;n+=1){const o=get(makeTranscriptKey(e,t,n));if(o&&o.text&&o.text.match(/\S/g)){startTimeStored||(startTimeStored=new Date(o.startedAt),startTime=new Date(o.startedAt));const a={transcriptId:e,sessionIndex:t,speakerIndex:n,person:o.person in SPEAKER_NAME_MAP?SPEAKER_NAME_MAP[o.person]:o.person,startedAt:new Date(o.startedAt),endedAt:new Date(o.endedAt),image:o.image,text:o.text,highlight:o.highlight};r.push(a)}}return r},deleteTranscript=e=>{const t=get(makeTranscriptKey(e));for(let r=0;r<=t;r+=1){const t=get(makeTranscriptKey(e,r));for(let o=0;o<=t;o+=1)remove(makeTranscriptKey(e,r,o));remove(makeTranscriptKey(e,r))}remove(makeTranscriptKey(e)),get(makeTranscriptKey(`${e}_name`))&&remove(makeTranscriptKey(`${e}_name`));let r=get(KEY_TRANSCRIPT_IDS)||[],o=get(APPLICATION_SPEECH_IDS)||[];const n=r.indexOf(e),a=o.findIndex((t=>t.ext===e));r.splice(n,1),o.splice(a,1),debug("would set transcript to",r),debug("would set transcript pairs to",o),set(KEY_TRANSCRIPT_IDS,r),set(APPLICATION_SPEECH_IDS,o);const s=document.querySelector(`#${e}`);if(s){const e=s.parentNode;e.removeChild(s),0===e.children.length&&(e.parentNode.removeChild(e.previousSibling),e.parentNode.removeChild(e))}else debug(`transcriptNode doesn't exist for ${e}`)},deletePreviousTranscripts=()=>{let e=get(KEY_TRANSCRIPT_IDS)||[];if(e.length>1)for(let t of e)t!==currentTranscriptId&&deleteTranscript(t)};
const makeFullKey=e=>`__laxis_${e}`,makeTranscriptKey=(...e)=>{const[t,r,o]=e,n=[`hangout_${t}`];return e.length>=2&&(n.push(`session_${r}`),e.length>=3&&n.push(`speaker_${o}`)),n.join("_")},get=e=>{const t=window.localStorage.getItem(makeFullKey(e));return"string"==typeof t||t instanceof String?(debug(e,t),JSON.parse(t)):t},set=(e,t,r=!1)=>{const o=makeFullKey(e),n=JSON.stringify(t),a=makeFullKey("rotationKeys");let s=JSON.parse(window.localStorage.getItem(a))||[],i=new Set(s);for(;;)try{window.localStorage.setItem(o,n),r&&(i.add(o),window.localStorage.setItem(a,JSON.stringify(Array.from(i))));break}catch(e){if(e instanceof DOMException&&("QuotaExceededError"===e.name||e.code===DOMException.QUOTA_EXCEEDED_ERR)){if(console.log("Local storage quota exceeded! Deleting old keys to make room for new ones. This may take a while..."),debug("Local storage quota exceeded! Deleting old keys to make room for new ones. This may take a while..."),i.size>0){for(let e=0;e<5&&i.size>0;e++){const e=i.values().next().value;i.delete(e),window.localStorage.removeItem(e)}window.localStorage.setItem(a,JSON.stringify(Array.from(i)));continue}console.error("No keys available to delete.");break}console.error("Unexpected error:",e);break}},remove=e=>{debug(`remove ${makeFullKey(e)}`),window.localStorage.removeItem(makeFullKey(e))},getOrSet=(e,t)=>{const r=get(e);return null==r?(set(e,t),t):r},increment=e=>{const t=get(e);if(null==t)return set(e,0),0;{let r=t+1;return set(e,r),r}},setSpeaker=e=>{set(makeTranscriptKey(e.transcriptId,e.sessionIndex,e.speakerIndex),{image:e.image,person:e.person,text:e.text,startedAt:e.startedAt,endedAt:e.endedAt,highlight:e.highlight},!0);try{chrome.runtime.sendMessage({type:"TRANSCRIPT_UPDATE",sessionId:String(e.transcriptId),speaker:e.person,text:e.text,startedAt:e.startedAt,endedAt:e.endedAt})}catch(t){}},getTranscript=e=>{const t=get(makeTranscriptKey(e))||0;let r=[];const o=get(makeTranscriptKey(e,t))||0;for(let n=0;n<=o;n+=1){const o=get(makeTranscriptKey(e,t,n));if(o&&o.text&&o.text.match(/\S/g)){startTimeStored||(startTimeStored=new Date(o.startedAt),startTime=new Date(o.startedAt));const a={transcriptId:e,sessionIndex:t,speakerIndex:n,person:o.person in SPEAKER_NAME_MAP?SPEAKER_NAME_MAP[o.person]:o.person,startedAt:new Date(o.startedAt),endedAt:new Date(o.endedAt),image:o.image,text:o.text,highlight:o.highlight};r.push(a)}}return r},deleteTranscript=e=>{const t=get(makeTranscriptKey(e));for(let r=0;r<=t;r+=1){const t=get(makeTranscriptKey(e,r));for(let o=0;o<=t;o+=1)remove(makeTranscriptKey(e,r,o));remove(makeTranscriptKey(e,r))}remove(makeTranscriptKey(e)),get(makeTranscriptKey(`${e}_name`))&&remove(makeTranscriptKey(`${e}_name`));let r=get(KEY_TRANSCRIPT_IDS)||[],o=get(APPLICATION_SPEECH_IDS)||[];const n=r.indexOf(e),a=o.findIndex((t=>t.ext===e));r.splice(n,1),o.splice(a,1),debug("would set transcript to",r),debug("would set transcript pairs to",o),set(KEY_TRANSCRIPT_IDS,r),set(APPLICATION_SPEECH_IDS,o);const s=document.querySelector(`#${e}`);if(s){const e=s.parentNode;e.removeChild(s),0===e.children.length&&(e.parentNode.removeChild(e.previousSibling),e.parentNode.removeChild(e))}else debug(`transcriptNode doesn't exist for ${e}`)},deletePreviousTranscripts=()=>{let e=get(KEY_TRANSCRIPT_IDS)||[];if(e.length>1)for(let t of e)t!==currentTranscriptId&&deleteTranscript(t)};
@@ -111,21 +111,17 @@ reDisplayPrompt = () => {
e && (e.style.display = "block"), chrome.storage.local.remove("token")
},
addRemindLogin = () => {
console.log("add login");
const e = document.getElementById("laxis-miniPanel"),
t = document.getElementById("laxis-expandPanel"),
o = document.createElement("div");
o.title = "Login to autosave notes", o.id = "laxis-remindLogin", o.style.width = "40px", o.style.height = "40px", o.classList.add("miniButtonContainer"), o.style.border = "0", o.addEventListener("click", signup), o.style.padding = "0";
const n = createRemindLoginIcon();
n.id = "remindLoginIcon", o.appendChild(n), n.id = "remindLoginIcon", o.style.display = "none", e.insertBefore(o, t)
// غیرفعال شد: دکمه‌ی «Login to autosave» مربوط به laxis cloud بود.
},
reDisplayRemindLogin = () => {
console.log("redisplay");
const e = document.getElementById("laxis-remindLogin");
e && (e.style.display = "block"), chrome.storage.local.remove("token")
},
getTopics = (e, t) => window.fetch(`${domainUrl}/api/v2/templates/${e}/topics`, t),
getTopics = (e, t) => Promise.reject(new Error("Laxis cloud disabled")),
Export2App = async (e, t = !1) => new Promise(((o, n) => {
// غیرفعال شد: آپلود transcript به laxis cloud حذف شده است. هیچ داده‌ای بیرون نمی‌رود.
return n(new Error("Laxis cloud disabled"));
chrome.storage.local.get(["token"], (function(i) {
if (i.token) {
let s = document.getElementById("laxis-openDownloadMenu"),
@@ -260,10 +256,10 @@ Export2App = async (e, t = !1) => new Promise(((o, n) => {
a = () => Export2Txt(s, e);
break;
default:
a = () => Export2App(o)
a = () => Export2Txt(s, e)
}
a().then((e => {
"app" === i && window.open(`${domainUrl}/transcript/${e}`, "_blank")
a().then((() => {
// قبلاً برای گزینه‌ی «app» صفحه‌ی laxis cloud باز می‌شد؛ حذف شد.
})).catch((e => {
window.alert(e);
const t = get(ERROR_SAVING) || [];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

+24 -116
View File
@@ -1,127 +1,35 @@
// login.js — service worker افزونه.
//
// نسخه‌ی پاک‌سازی‌شده: تمام ارتباط با laxis cloud حذف شده است.
// دیگر هیچ توکنی دریافت نمی‌شود و هیچ transcript ای به سروری بیرون فرستاده نمی‌شود.
// این ورکر فقط:
// ۱) اسکریپت‌های لازم (share, panel) و bridge محلی را import می‌کند،
// ۲) چند مقدار وضعیتِ لوکال (meetingId / meetingName / username) را نگه می‌دارد،
// ۳) bridge.js را بالا می‌آورد تا caption ها به سرور لوکال (ws://127.0.0.1:8765) برسند.
try {
importScripts("config/share.js"), importScripts("config/panel.js")
importScripts("config/share.js"), importScripts("config/panel.js"), importScripts("bridge.js")
} catch (e) {
console.error(e)
}
let meetingId = null,
transcriptId = null,
meetingName = null,
username = "";
chrome.storage.session.setAccessLevel({
accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS"
});
const getTopics = (e, t) => fetch(`${domainUrl}/api/v2/templates/${e}/topics`, t);
chrome.tabs.onRemoved.addListener(((e, t) => {
chrome.storage.session.get(["sessionList"], (e => {
if (e.sessionList) {
let t = e.sessionList;
console.log(t), chrome.storage.local.get(["token"], (e => {
if (e.token) {
let o = e.token;
fetch(`${domainUrl}/api/v2/templates?quick-note=true`, {
method: "GET",
headers: {
Authorization: `Bearer ${o}`,
"Content-Type": "application/json"
}
}).then((e => {
200 === e.status ? e.json().then((e => {
e.items.length ? getTopics(e.items[0].id, {
method: "GET",
headers: {
Authorization: `Bearer ${o}`,
"Content-Type": "application/json"
}
}).then((s => {
s.json().then((s => {
const n = s.items;
let r = [];
if (t.forEach((({
image: e,
person: t,
text: o,
startedAt: s,
endedAt: i,
highlight: a
}) => {
let c = [];
if (a) {
const e = bookmarkList.find((e => e.color === a[0]));
if (e) {
const t = n.find((t => t.color.toLowerCase() === e.code.toLowerCase()));
t && (c = [t.id])
}
}
o && r.push({
imageUrl: e,
person: "You" === t ? username : t,
startedAt: s,
endedAt: i || s,
highlights: c,
text: o
})
})), r.length) {
let t = JSON.stringify({
meetingId: meetingId,
meetingName: meetingName,
templateId: e.items[0].id,
transcripts: r,
isEnded: !0
}),
s = {
Authorization: `Bearer ${o}`,
"Content-Type": "application/json"
};
transcriptId ? fetch(`${domainUrl}/api/v1/speeches/google-meet/${transcriptId}`, {
method: "PUT",
headers: s,
body: t
}).then((e => {
200 === e.status ? (console.log("save success"), chrome.tabs.create({
url: `${domainUrl}/transcript/${transcriptId}`
})) : 401 === e.status ? console.log("Expired token") : e.json().then((e => {
console.log(e.message)
}))
})).catch((e => {
console.log(e)
})) : fetch(`${domainUrl}/api/v1/speeches/google-meet`, {
method: "POST",
headers: s,
body: t
}).then((e => {
200 === e.status ? (chrome.tabs.create({
url: `${domainUrl}/transcript/${transcriptId}`
}), console.log("success")) : 401 === e.status ? console.log("Expired token") : e.json().then((e => {
console.log(e.message)
}))
})).catch((e => {
console.log(e)
}))
} else console.log("Empty transcript")
}))
})).catch((e => reject(e))) : console.log("No quick note template")
})) : 401 === e.status ? console.log("Expired token") : e.json().then((e => console.log(e.message)))
}))
}
}))
}
}))
})), chrome.runtime.onMessage.addListener((e => {
console.log(e), "meetingId" === e.type && (console.log(e.meetingId), meetingId = e.meetingId), "transcriptId" === e.type && (console.log(e.transcriptId), transcriptId = e.transcriptId), "meetingName" === e.type && (meetingName = e.meetingName), "username" === e.type && (username = e.data)
})), chrome.runtime.onInstalled.addListener((e => {
// e.reason === chrome.runtime.OnInstalledReason.INSTALL && chrome.tabs.create({
// url: signupUrl
// }, (function(e) {}))
})), chrome.runtime.onMessageExternal.addListener((function(e, t, o) {
t.origin === domainUrl && (o("extension received the token"), chrome.storage.local.set({
token: e.token,
refreshToken: e.refreshToken
}, (function() {})), chrome.tabs.query({
url: googleMeetUrl
}, (function(t) {
t[0] && chrome.tabs.sendMessage(t[0].id, {
token: e.token,
refreshToken: e.refreshToken
}, (function(e) {}))
})))
})), chrome.runtime.setUninstallURL("https://www.laxis.tech/uninstall-survey");
// فقط نگه‌داشتن وضعیتِ لوکال؛ هیچ درخواست شبکه‌ای انجام نمی‌شود.
chrome.runtime.onMessage.addListener((e => {
if (!e || !e.type) return;
if ("meetingId" === e.type) meetingId = e.meetingId;
else if ("transcriptId" === e.type) transcriptId = e.transcriptId;
else if ("meetingName" === e.type) meetingName = e.meetingName;
else if ("username" === e.type) username = e.data;
}));
// در نسخه‌ی laxis این‌جا صفحه‌ی signup باز می‌شد؛ حالا عمداً خالی است.
chrome.runtime.onInstalled.addListener((() => {}));
@@ -1,9 +1,7 @@
{
"update_url": "https://clients2.google.com/service/update2/crx",
"manifest_version": 3,
"name": "Google Meet Transcripts & AI Summary",
"description": "Google Meet Transcription, AI Summary and Insight. Get the most out of Google Meet!",
"name": "Meet Transcripts (Local Bridge)",
"description": "Capture Google Meet captions locally and stream them to a local bridge. No cloud, no login, no tracking.",
"version": "4.3.12",
"icons": {
"16": "image/logo16x16.png",
@@ -57,5 +55,5 @@
}
],
"permissions": ["storage"],
"host_permissions": ["https://meet.google.com/*"]
"host_permissions": ["https://meet.google.com/*", "ws://127.0.0.1:8765/*"]
}
+29 -1
View File
@@ -1 +1,29 @@
addRoot(),addMiniPanel(),addRemindLogin();const checkOngoingMeeting=setInterval(tryTo(addCaptionPanel,"adding button"),1e3);let notificationsTimeout;const notificationsTimeoutDuration=3e3;let checkCaptionStatusInterval;function saveLog(e){const t={message:e,time:(new Date).toISOString()};chrome.storage.local.get("logs",(function(e){var n=structuredClone(e.logs);void 0===n?n=[t]:n.push(t),chrome.storage.local.set({logs:n})}))}syncSettings(),window.addEventListener("click",(e=>{const t=document.getElementById("laxis-downloadMenu");e.target===t&&(t.style.display="none")})),chrome.runtime.onMessage.addListener((function(e,t,n){if(t.id===extensionId)if(e.token){n("token received by content script");const t={method:"GET",headers:{Authorization:`Bearer ${e.token}`}},o=()=>window.fetch(`${domainUrl}/api/v1/users/info`,t),i=()=>window.fetch(`${domainUrl}/api/v2/templates?quick-note=true`,t),s=e=>window.fetch(`${domainUrl}/api/v2/templates/${e}/topics`,t);Promise.all([o(),i()]).then((t=>{if(200===t[0].status&&200===t[1].status){loginStatus=1;const e=document.getElementById("login-prompt");e&&(e.style.display="none");const n=document.getElementById("laxis-remindLogin");n&&(n.style.display="none"),t[0].json().then((e=>{appUser=`${e.firstName} ${e.lastName}`,chrome.runtime.sendMessage({type:"username",data:`${e.firstName} ${e.lastName}`})})),t[1].json().then((e=>{let t=bookmarkList;s(e.items[0].id).then((e=>e.json().then((e=>{e.items.forEach((e=>{let n=bookmarkList.findIndex((t=>t.code===e.color));if(-1!==n){if(t[n].name=e.name,document.getElementById(`laxis-highlight-${e.color}-title`)){document.getElementById(`laxis-highlight-${e.color}-title`).innerHTML=e.name}let o=document.getElementById(`laxis-highlight-${e.color}`),i=document.getElementById(`laxis-highlight-${e.color}-mini`);o&&(o.title=`Highlight as ${e.name}`),i&&(i.title=`Highlight as ${e.name}`)}}))})))).catch((e=>console.warn(e))),bookmarkList=t}))}else 401===t[0].status||401===t[1].status?(401===t[0].status?t[0].json().then((t=>{saveLog(`chrome.runtime.onMessage.addListener 401 fail ${t.message} ${e.token}`)})):t[1].json().then((t=>{saveLog(`chrome.runtime.onMessage.addListener 401 fail ${t.message} ${e.token}`)})),reDisplayPrompt(),reDisplayRemindLogin()):t[0].json().then((e=>{window.alert(e.message)}))})).catch((e=>{window.alert(e)}))}else saveLog(`chrome.runtime.onMessage.addListener token empty fail ${e.token}`),reDisplayRemindLogin()}));
// runtime.js — نقطه‌ی شروع content script (آخرین فایل لودشده).
//
// نسخه‌ی پاک‌سازی‌شده:
// - فراخوانی addRemindLogin() حذف شد (دیگر دکمه‌ی «Login to autosave» ساخته نمی‌شود).
// - listener دریافت توکن و fetch به laxis (users/info, templates) کاملاً حذف شد.
// بقیه‌ی منطق (ساخت پنل، مانیتور جلسه، تنظیمات، منوی دانلودِ محلی) دست‌نخورده است.
addRoot();
addMiniPanel();
const checkOngoingMeeting = setInterval(tryTo(addCaptionPanel, "adding button"), 1e3);
let notificationsTimeout;
const notificationsTimeoutDuration = 3e3;
let checkCaptionStatusInterval;
function saveLog(e) {
const t = { message: e, time: (new Date).toISOString() };
chrome.storage.local.get("logs", (function(e) {
var n = structuredClone(e.logs);
void 0 === n ? n = [t] : n.push(t), chrome.storage.local.set({ logs: n })
}))
}
syncSettings();
window.addEventListener("click", (e => {
const t = document.getElementById("laxis-downloadMenu");
e.target === t && (t.style.display = "none")
}));