discovery-media-player 0.1.41 → 0.1.43
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/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/handler.js +143 -16
- package/server/presentations.js +23 -2
- package/server/shared.generated.js +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
|
|
5
5
|
// player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
|
|
6
6
|
"use strict";
|
|
7
|
-
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var j=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)j(e,t,{get:n[t],enumerable:!0})},Te=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of xe(n))!we.call(e,o)&&o!==t&&j(e,o,{get:()=>n[o],enumerable:!(r=Me(n,o))||r.enumerable});return e};var _e=e=>Te(j({},\"__esModule\",{value:!0}),e);var vn={};y(vn,{bridge:()=>L,chat:()=>O,live:()=>$,presentation:()=>N,presentationState:()=>D,tracking:()=>C,viewer:()=>H});var L={};y(L,{onHostMessage:()=>$e,onPlayerMessage:()=>Ce,parseHostMessage:()=>pe,parsePlayerMessage:()=>de,sendToHost:()=>Ae,sendToPlayer:()=>Re,toWire:()=>K});var G=\"3dd-doc-\",ke=/^[A-Za-z0-9_-]{1,64}$/,Se=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ee=[\"embed-denied\"],Ie=[\"present-invite\",\"present-handover\",\"present-switch\"],Pe=[\"handover-done\"];function K(e){let n={type:G+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function ce(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(G))return null;let o=r.slice(G.length);if(n.includes(o))return{type:o};if(Ee.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!ke.test(s)?null:{type:o,slug:s}}function de(e){return ce(e,Se,Ie)}function pe(e){return ce(e,Pe,[])}function Ae(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(K(e),\"*\")}catch(t){}}function Re(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(n),\"*\")}catch(r){}}function Le(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Ce(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Le(t,s.source))return;let a=de(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function $e(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let c=pe(a.data);c&&e(c)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var C={};y(C,{createTracker:()=>Ve});var Oe=Math.ceil(300),Ne=25,xn=Oe*Ne,fe=18e4;var De=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function Ue(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function We(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function Ve(e={}){var ae,le;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ae=e.idleMs)!=null?ae:fe,a=(le=e.sessionEveryMs)!=null?le:12e3,c=e.slug||\"\",d=e.internal||null,m=e.sessionId||Ue(),b=e.send||We(o,n),f={},h=0,M=0,u=0,v=0,g=null,_=r(),k=!1,U=!1,A=[],ve=()=>!!c||!!d,te=i=>{if(ve())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!k,re=i=>{let l=r()-i,x=Math.max(0,_-i)+s;return Math.max(0,Math.min(l,x))},V=()=>{g!=null&&h>0&&(f[h]=(f[h]||0)+re(g)/1e3),g=W()?r():null},oe=()=>{V(),g=null},F=()=>{g==null&&W()&&(g=r())},z=()=>{let i=0;for(let l in f)i+=f[l];return g!=null&&(i+=re(g)/1e3),Math.round(i)},ie=()=>{let i={};for(let l in f)i[l]=Math.round(f[l]);return i},S=()=>{_=r(),k&&(k=!1,F())},se=(i,l)=>{d||te({slug:c,event:i,page:l||M,maxPage:u,seconds:z(),sessionId:m})},R=()=>{V();let i={event:\"session\",sessionId:m,numPages:v,maxPage:u,totalSeconds:z(),pagesTime:ie()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=c,te(i)},E=(i,l,x,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,x,ue),A.push(()=>{i.removeEventListener(l,x,ue)}))};return{sessionId:m,totalSeconds:z,maxPage:()=>u,pageTimes:ie,start(){if(U)return;U=!0,se(\"open\",1),h=1,_=r(),g=W()?r():null,E(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(oe(),R()):(S(),F())}),E(n,\"focus\",()=>{S(),F()});for(let x of De)E(n,x,S,{passive:!0});E(e.scrollElement,\"scroll\",S,{passive:!0}),E(n,\"beforeunload\",R);let i=n.setInterval(()=>{!k&&r()-_>s&&(k=!0,oe())},5e3),l=n.setInterval(R,a);A.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(_=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:S,flush:R,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(l){}}U=!1}}}var $={};y($,{STORAGE_KEYS:()=>I,attendeeKey:()=>Ge,authorToken:()=>Be,avatarHtml:()=>B,canModerate:()=>q,createScheduler:()=>Xe,escapeHtml:()=>p,flattenPresence:()=>Fe,formatMessageBody:()=>Z,initials:()=>me,isMentioned:()=>Ze,isMine:()=>w,reactorId:()=>Y,shouldNotify:()=>Ye,unreadLabel:()=>qe});var I={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function p(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function me(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function B(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(me(n))}function Z(e){let n=p(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function Fe(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let s=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[s]?o.role===\"presenter\"&&(n[s]=o):(n[s]=o,t.push(s))}return t.map(r=>n[r])}function ze(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function je(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function Ge(e,n,t=\"anon\",r=()=>Math.random().toString(36).slice(2,10)){if(e!=null&&e.email)return String(e.email).toLowerCase();try{if(!n)return`anon-${t}`;let o=n.getItem(I.attendeeKey);if(o)return o;let s=`anon-${r()}`;return n.setItem(I.attendeeKey,s),s}catch(o){return`anon-${t}`}}function Ke(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Be(e,n=Ke){let t=ze(e,I.authorToken);if(t)return t;let r=n();return je(e,I.authorToken,r),r}function Y(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function w(e,n){return!e||!n?!1:e.author_email&&n.email?e.author_email===n.email:!e.author_email&&!n.email?e.author_name===n.name:!1}function q(e){return!!(e&&e.role===\"presenter\")}function Ze(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!w(e,n):!1}function Ye(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||w(n,t)?!1:o}function qe(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function Xe(e,n={}){var M;let t=(M=n.minMs)!=null?M:300,r=n.now||(()=>Date.now()),o=n.setTimer||((u,v)=>setTimeout(u,v)),s=n.clearTimer||(u=>clearTimeout(u)),a=!1,c=!1,d=-1/0,m=null,b=!1;function f(){if(b||a){c=!0;return}let u=t-(r()-d);if(u>0){h(u);return}a=!0,c=!1,d=r();let v=!1;e(()=>{v||(v=!0,a=!1,c&&!b&&f())})}function h(u){b||m!==null||(m=o(()=>{m=null,f()},Math.max(0,u)))}return{signaler(){b||f()},maintenant(){b||(d=-1/0,f())},arreter(){b=!0,m!==null&&(s(m),m=null)}}}var O={};y(O,{messageClassName:()=>Qe,renderActions:()=>ye,renderAttachment:()=>be,renderMessage:()=>Je,renderReactions:()=>ge});function ge(e,n){let t=e&&e.reactions||{},r=Y(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let c=a.indexOf(r)>=0;o+=`<button class=\"re-chip${c?\" on\":\"\"}\" data-e=\"${p(s)}\">${p(s)} ${a.length}</button>`}return o}function be(e){if(!e||!e.url)return\"\";let n=p(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=p(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function ye(e,n){let t=w(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||q(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Je(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${p(e.reply_name||\"\")}</b> ${p(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${Z(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${B(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${p(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+be(e.attachment)+`<div class=cm-re>${ge(e,n.me)}</div></span><span class=cm-act>${ye(e,n)}</span>`}function Qe(e,n,t){return\"cm\"+(w(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var N={};y(N,{DEFAULT_CENTER:()=>Q,DEFAULT_ZOOM:()=>ee,MAP_TYPES:()=>P,cycleMapType:()=>en,initialMapContent:()=>tn,mapTypeLabel:()=>nn,sanitizeContent:()=>ne});var P=[\"roadmap\",\"satellite\",\"hybrid\"],Q=[46.6,2.5],ee=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function X(e){if(!Array.isArray(e))return null;let n=T(e[0]),t=T(e[1]);return n!=null&&t!=null?[n,t]:null}var J=(e,n,t)=>Math.max(n,Math.min(t,e));function ne(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=X(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:T(r.heading)||0,pitch:J(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:J(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:X(n.center)||[...Q],zoom:J(Math.trunc(T(n.zoom)||ee),1,21),marker:X(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function en(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function nn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function tn(){return{kind:\"map\",center:[...Q],zoom:ee,marker:null,mapType:null,label:null}}var H={};y(H,{CURRENT_PAGE_MARGIN:()=>un,DEFAULT_ASPECT:()=>sn,MAX_ZOOM:()=>on,MIN_PAGE_WIDTH:()=>an,MIN_ZOOM:()=>rn,PRERENDER_MARGIN:()=>ln,arrowState:()=>pn,averageColor:()=>gn,clampPage:()=>he,clampZoom:()=>cn,fitWidth:()=>mn,isImageDocument:()=>bn,progressPercent:()=>dn});var rn=.5,on=3,sn=1.35,an=280,ln=\"500px 0px\",un=\"-48% 0px -48% 0px\";function cn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function he(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function dn(e,n){return n?Math.round(he(e,n)/n*100):0}function pn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var fn=28;function mn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-fn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function gn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var D={};y(D,{presentationTransition:()=>yn,switchDocUrl:()=>hn});function yn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=ne(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function hn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return _e(vn);})();", SOURCE_HASH: "48e673113c31267d" };
|
|
7
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var j=Object.defineProperty;var _e=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var Te=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)j(e,t,{get:n[t],enumerable:!0})},Ee=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of we(n))!Te.call(e,o)&&o!==t&&j(e,o,{get:()=>n[o],enumerable:!(r=_e(n,o))||r.enumerable});return e};var Se=e=>Ee(j({},\"__esModule\",{value:!0}),e);var wn={};y(wn,{bridge:()=>C,chat:()=>N,live:()=>O,presentation:()=>$,presentationState:()=>U,tracking:()=>L,viewer:()=>H});var C={};y(C,{onHostMessage:()=>$e,onPlayerMessage:()=>Ne,parseHostMessage:()=>pe,parsePlayerMessage:()=>de,sendToHost:()=>Ce,sendToPlayer:()=>Le,toWire:()=>K});var G=\"3dd-doc-\",ke=/^[A-Za-z0-9_-]{1,64}$/,Re=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ie=[\"embed-denied\"],Pe=[\"present-invite\",\"present-handover\",\"present-switch\"],Ae=[\"handover-done\"];function K(e){let n={type:G+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function ce(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(G))return null;let o=r.slice(G.length);if(n.includes(o))return{type:o};if(Ie.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!ke.test(s)?null:{type:o,slug:s}}function de(e){return ce(e,Re,Pe)}function pe(e){return ce(e,Ae,[])}function Ce(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(K(e),\"*\")}catch(t){}}function Le(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(n),\"*\")}catch(r){}}function Oe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Ne(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Oe(t,s.source))return;let a=de(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function $e(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let c=pe(a.data);c&&e(c)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var L={};y(L,{createTracker:()=>Ke});var He=Math.ceil(300),fe=25,En=He*fe,me=18e4,Ue=25e3,De=Math.ceil(36e5/Ue),We=720,Ve=De+We,Sn=Ve*fe;var ze=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function je(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function Ge(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function Ke(e={}){var ae,le;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ae=e.idleMs)!=null?ae:me,a=(le=e.sessionEveryMs)!=null?le:12e3,c=e.slug||\"\",d=e.internal||null,m=e.sessionId||je(),b=e.send||Ge(o,n),f={},h=0,M=0,u=0,v=0,g=null,T=r(),E=!1,D=!1,P=[],xe=()=>!!c||!!d,te=i=>{if(xe())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!E,re=i=>{let l=r()-i,x=Math.max(0,T-i)+s;return Math.max(0,Math.min(l,x))},V=()=>{g!=null&&h>0&&(f[h]=(f[h]||0)+re(g)/1e3),g=W()?r():null},oe=()=>{V(),g=null},F=()=>{g==null&&W()&&(g=r())},z=()=>{let i=0;for(let l in f)i+=f[l];return g!=null&&(i+=re(g)/1e3),Math.round(i)},ie=()=>{let i={};for(let l in f)i[l]=Math.round(f[l]);return i},S=()=>{T=r(),E&&(E=!1,F())},se=(i,l)=>{d||te({slug:c,event:i,page:l||M,maxPage:u,seconds:z(),sessionId:m})},A=()=>{V();let i={event:\"session\",sessionId:m,numPages:v,maxPage:u,totalSeconds:z(),pagesTime:ie()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=c,te(i)},k=(i,l,x,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,x,ue),P.push(()=>{i.removeEventListener(l,x,ue)}))};return{sessionId:m,totalSeconds:z,maxPage:()=>u,pageTimes:ie,start(){if(D)return;D=!0,se(\"open\",1),h=1,T=r(),g=W()?r():null,k(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(oe(),A()):(S(),F())}),k(n,\"focus\",()=>{S(),F()});for(let x of ze)k(n,x,S,{passive:!0});k(e.scrollElement,\"scroll\",S,{passive:!0}),k(n,\"beforeunload\",A);let i=n.setInterval(()=>{!E&&r()-T>s&&(E=!0,oe())},5e3),l=n.setInterval(A,a);P.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(T=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:S,flush:A,stop(){for(;P.length;){let i=P.pop();try{i==null||i()}catch(l){}}D=!1}}}var O={};y(O,{STORAGE_KEYS:()=>R,attendeeKey:()=>qe,authorToken:()=>Xe,avatarHtml:()=>Y,canModerate:()=>q,createScheduler:()=>nn,escapeHtml:()=>p,flattenPresence:()=>Ye,formatMessageBody:()=>B,initials:()=>ge,isMentioned:()=>Qe,isMine:()=>_,reactorId:()=>Z,shouldNotify:()=>Je,unreadLabel:()=>en});var R={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function p(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function ge(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function Y(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(ge(n))}function B(e){let n=p(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function Ye(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let s=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[s]?o.role===\"presenter\"&&(n[s]=o):(n[s]=o,t.push(s))}return t.map(r=>n[r])}function Be(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function Ze(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function qe(e,n=\"anon\",t=be){try{if(!e)return`anon-${n}`;let r=e.getItem(R.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(R.attendeeKey,o),o}catch(r){return`anon-${n}`}}function be(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Xe(e,n=be){let t=Be(e,R.authorToken);if(t)return t;let r=n();return Ze(e,R.authorToken,r),r}function Z(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function _(e,n){return!e||!n?!1:e.author_email&&n.email?e.author_email===n.email:!e.author_email&&!n.email?e.author_name===n.name:!1}function q(e){return!!(e&&e.role===\"presenter\")}function Qe(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!_(e,n):!1}function Je(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function en(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function nn(e,n={}){var M;let t=(M=n.minMs)!=null?M:300,r=n.now||(()=>Date.now()),o=n.setTimer||((u,v)=>setTimeout(u,v)),s=n.clearTimer||(u=>clearTimeout(u)),a=!1,c=!1,d=-1/0,m=null,b=!1;function f(){if(b||a){c=!0;return}let u=t-(r()-d);if(u>0){h(u);return}a=!0,c=!1,d=r();let v=!1;e(()=>{v||(v=!0,a=!1,c&&!b&&f())})}function h(u){b||m!==null||(m=o(()=>{m=null,f()},Math.max(0,u)))}return{signaler(){b||f()},maintenant(){b||(d=-1/0,f())},arreter(){b=!0,m!==null&&(s(m),m=null)}}}var N={};y(N,{messageClassName:()=>rn,renderActions:()=>ve,renderAttachment:()=>he,renderMessage:()=>tn,renderReactions:()=>ye});function ye(e,n){let t=e&&e.reactions||{},r=Z(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let c=a.indexOf(r)>=0;o+=`<button class=\"re-chip${c?\" on\":\"\"}\" data-e=\"${p(s)}\">${p(s)} ${a.length}</button>`}return o}function he(e){if(!e||!e.url)return\"\";let n=p(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=p(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function ve(e,n){let t=_(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||q(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function tn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${p(e.reply_name||\"\")}</b> ${p(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${B(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${Y(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${p(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+he(e.attachment)+`<div class=cm-re>${ye(e,n.me)}</div></span><span class=cm-act>${ve(e,n)}</span>`}function rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var $={};y($,{DEFAULT_CENTER:()=>J,DEFAULT_ZOOM:()=>ee,MAP_TYPES:()=>I,cycleMapType:()=>on,initialMapContent:()=>an,mapTypeLabel:()=>sn,sanitizeContent:()=>ne});var I=[\"roadmap\",\"satellite\",\"hybrid\"],J=[46.6,2.5],ee=6;function w(e){let n=Number(e);return Number.isFinite(n)?n:null}function X(e){if(!Array.isArray(e))return null;let n=w(e[0]),t=w(e[1]);return n!=null&&t!=null?[n,t]:null}var Q=(e,n,t)=>Math.max(n,Math.min(t,e));function ne(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=X(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:w(r.heading)||0,pitch:Q(w(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:Q(w(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:X(n.center)||[...J],zoom:Q(Math.trunc(w(n.zoom)||ee),1,21),marker:X(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function on(e){let n=I.indexOf(e);return I[(n+1)%I.length]}function sn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function an(){return{kind:\"map\",center:[...J],zoom:ee,marker:null,mapType:null,label:null}}var H={};y(H,{CURRENT_PAGE_MARGIN:()=>fn,DEFAULT_ASPECT:()=>cn,MAX_ZOOM:()=>un,MIN_PAGE_WIDTH:()=>dn,MIN_ZOOM:()=>ln,PRERENDER_MARGIN:()=>pn,arrowState:()=>bn,averageColor:()=>vn,clampPage:()=>Me,clampZoom:()=>mn,fitWidth:()=>hn,isImageDocument:()=>Mn,progressPercent:()=>gn});var ln=.5,un=3,cn=1.35,dn=280,pn=\"500px 0px\",fn=\"-48% 0px -48% 0px\";function mn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Me(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function gn(e,n){return n?Math.round(Me(e,n)/n*100):0}function bn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var yn=28;function hn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-yn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function vn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Mn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var U={};y(U,{presentationTransition:()=>xn,switchDocUrl:()=>_n});function xn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=ne(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function _n(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Se(wn);})();", SOURCE_HASH: "bcdddbf20bceaa1b" };
|
package/server/handler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const { getShareBySlug, logView, upsertSession, createReshare, sendReshareEmail, upsertInternalSession,
|
|
8
8
|
createShare, revokeShare, setShareAuth, overview: docOverview, listSharesForDoc, listSessionsForDoc, internalStatsForDoc } = require("./shares");
|
|
9
|
-
const { SESSION_QUOTA_PER_HOUR } = require("./shared.generated.js");
|
|
9
|
+
const { SESSION_QUOTA_PER_HOUR, PRESENT_QUOTA_PER_HOUR } = require("./shared.generated.js");
|
|
10
10
|
const { createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent } = require("./presentations");
|
|
11
11
|
// CONTEXTE INJECTÉ : tout ce que le player emprunte à l'application hôte passe par ici — stockage,
|
|
12
12
|
// base, identité, limites, marque, journalisation — et rien d'autre. C'est la frontière qui permettra
|
|
@@ -282,14 +282,14 @@ var Live=(function(){
|
|
|
282
282
|
var MYID=Math.random().toString(36).slice(2,9);
|
|
283
283
|
function _store(){try{return window.localStorage;}catch(e){return null;}}
|
|
284
284
|
// Clé d'assistance STABLE (analytics de présentation) : email si connu, sinon id persistant par navigateur.
|
|
285
|
-
function attKey(
|
|
285
|
+
function attKey(){ return Player.live.attendeeKey(_store(),MYID); }
|
|
286
286
|
// Heartbeat d'assistance → le serveur journalise qui suit, combien de temps, et les pages vues (via la page
|
|
287
287
|
// courante de la présentation). Envoyé à la connexion puis toutes les 25 s. Best-effort (silencieux).
|
|
288
288
|
// Le serveur ne croit plus 'isMember' ni 'isPresenter' sur parole : l'appartenance se prouve par
|
|
289
289
|
// le jeton d'acces de la session, le titre de presentateur par le control_token. Ce qui part d'ici
|
|
290
290
|
// n'est plus qu'une AFFIRMATION, et le serveur la remplace par ce qu'il a verifie.
|
|
291
291
|
function sendAttend(){ if(!SLUG||!ME)return; try{ var h={'Content-Type':'application/json'};var jw=accessToken();if(jw)h.Authorization='Bearer '+jw;
|
|
292
|
-
fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-attend',slug:SLUG,control:CONTROL,key:attKey(
|
|
292
|
+
fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-attend',slug:SLUG,control:CONTROL,key:attKey(),name:ME.name||'',email:ME.email||'',avatar:ME.avatar||''})}); }catch(e){} }
|
|
293
293
|
var EMOJIS=['👍','❤️','😂','😮','👏','🎉'];
|
|
294
294
|
var RSVG='<svg viewBox="0 0 24 24" fill=none stroke=currentColor stroke-width=2 stroke-linecap=round><circle cx=12 cy=12 r=9 /><path d="M8.5 14.5s1.4 1.7 3.5 1.7 3.5-1.7 3.5-1.7"/><line x1=9 y1=9.2 x2=9.01 y2=9.2 /><line x1=15 y1=9.2 x2=15.01 y2=9.2 /></svg>';
|
|
295
295
|
function esc(s){return Player.live.escapeHtml(s);}
|
|
@@ -533,7 +533,7 @@ var Live=(function(){
|
|
|
533
533
|
// par accident : le jour où quelqu'un rebranche un paramètre, la charge d'un canal public
|
|
534
534
|
// redeviendrait crue sans que rien ne le signale. On coupe le chemin, pas seulement l'usage.
|
|
535
535
|
ch.on('broadcast',{event:'map'},function(){if(_onMap)_onMap();});
|
|
536
|
-
ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey(
|
|
536
|
+
ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey()});sendAttend();}});
|
|
537
537
|
_tyIv=setInterval(renderTyping,1500);
|
|
538
538
|
_atIv=setInterval(sendAttend,25000);
|
|
539
539
|
// Filet de sécurité : au déchargement de la page/iframe (fermeture, reload, switch), on retire la présence
|
|
@@ -1371,6 +1371,30 @@ const CLE_INVITE = "dmp-present-me";
|
|
|
1371
1371
|
*
|
|
1372
1372
|
* Signalé par la seconde passe d'audit (P1-6).
|
|
1373
1373
|
*/
|
|
1374
|
+
/**
|
|
1375
|
+
* La clé d'un participant qui ne peut RIEN prouver.
|
|
1376
|
+
*
|
|
1377
|
+
* Un anonyme n'a pas de jeton : sa clé vient forcément de son navigateur, et le serveur ne peut pas
|
|
1378
|
+
* la lui contester. Ce qu'il peut faire, c'est l'empêcher de sortir de son espace de noms — donc de
|
|
1379
|
+
* ressembler à la clé d'un membre, qui est une adresse e-mail.
|
|
1380
|
+
*
|
|
1381
|
+
* ⚠️ C'est la même logique que le quota fondé sur l'adresse : on ne fonde pas une garantie sur ce
|
|
1382
|
+
* que l'appelant choisit. Ici on ne peut pas éviter qu'il choisisse ; on peut borner ce qu'il peut
|
|
1383
|
+
* choisir. Deux anonymes restent séparés par une valeur imprévisible, comme deux sessions.
|
|
1384
|
+
*
|
|
1385
|
+
* Une clé absente ou hors forme n'est pas une erreur : elle vaut « inconnu ». Un participant mal
|
|
1386
|
+
* compté vaut mieux qu'une audience refusée.
|
|
1387
|
+
*/
|
|
1388
|
+
/** La clé d'un membre : son adresse, normalisée — la recherche de ligne est exacte. */
|
|
1389
|
+
function lcMembre(email) {
|
|
1390
|
+
return String(email || "").trim().toLowerCase();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function cleAnonyme(brut) {
|
|
1394
|
+
const v = String(brut == null ? "" : brut).trim().slice(0, 120);
|
|
1395
|
+
return /^anon-[A-Za-z0-9_-]{4,}$/.test(v) ? v : "anon-inconnu";
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1374
1398
|
async function profilDuJeton(req) {
|
|
1375
1399
|
try {
|
|
1376
1400
|
const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
|
|
@@ -2042,12 +2066,52 @@ ${LEGAL_CSS}
|
|
|
2042
2066
|
//
|
|
2043
2067
|
// sendBeacon ne s'attend pas, mais il rend la main une fois la requête MISE EN FILE : signaler
|
|
2044
2068
|
// juste après, puis couper, respecte l'ordre autant que ce transport le permet.
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2069
|
+
// FERMER UNE PRÉSENTATION EST UN ACTE, PAS UNE INTENTION.
|
|
2070
|
+
//
|
|
2071
|
+
// ⚠️ Cette fonction annonçait la fin avant de l'obtenir. « sendBeacon » ne rend AUCUNE réponse —
|
|
2072
|
+
// ni « enregistré » ni « refusé » — et pourtant l'interface se nettoyait, le jeton de contrôle
|
|
2073
|
+
// était effacé et le canal fermé dans la foulée. Si l'appel échouait, la présentation restait
|
|
2074
|
+
// VIVANTE pour l'audience pendant que le présentateur la croyait close ; et il n'avait même plus
|
|
2075
|
+
// de quoi la refermer, « clearCtl » ayant déjà jeté son jeton.
|
|
2076
|
+
//
|
|
2077
|
+
// ⚠️ Le beacon n'achetait rien ici : il sert à faire partir une requête pendant que la page
|
|
2078
|
+
// MEURT. Or le seul appelant était un bouton — on a tout le temps d'attendre. Il est désormais à
|
|
2079
|
+
// sa place, sur « pagehide », et là seulement.
|
|
2080
|
+
//
|
|
2081
|
+
// ⚠️ ET L'ORDRE COMPTE : l'audience ne relit l'état qu'APRÈS le 2xx. Diffuser avant, c'était lui
|
|
2082
|
+
// faire relire une présentation encore marquée active — elle rejouait donc l'ancien état.
|
|
2083
|
+
//
|
|
2084
|
+
// On n'efface donc rien tant que le serveur n'a pas confirmé : en cas d'échec le pilotage reste
|
|
2085
|
+
// entier et le présentateur peut réessayer. Une présentation vraiment abandonnée est rattrapée
|
|
2086
|
+
// par la péremption (STALE_MS, 3 min), qui existe exactement pour ce cas.
|
|
2087
|
+
//
|
|
2088
|
+
// Signalé par un audit externe.
|
|
2089
|
+
function endPresent(){
|
|
2090
|
+
if(!PRES) return Promise.resolve();
|
|
2091
|
+
var p=PRES, btn=document.getElementById('pbarEnd'), libelle=btn?btn.textContent:'';
|
|
2092
|
+
if(btn){ btn.disabled=true; btn.textContent='…'; }
|
|
2093
|
+
return fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,
|
|
2094
|
+
body:JSON.stringify({action:'present-end',slug:p.slug,control:p.control})})
|
|
2095
|
+
.then(function(r){ if(!r||!r.ok) throw new Error('present-end'); })
|
|
2096
|
+
.then(function(){
|
|
2097
|
+
PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
|
|
2098
|
+
var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
|
|
2099
|
+
try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){}
|
|
2100
|
+
}, function(){
|
|
2101
|
+
if(btn){ btn.disabled=false; btn.textContent=libelle||'Terminer'; btn.title='La fin n’a pas été enregistrée — réessayez.'; }
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
// ⚠️ LE SEUL ENDROIT OÙ « sendBeacon » A UN SENS : la page s'en va, aucune réponse ne pourra être
|
|
2105
|
+
// lue, et une présentation laissée ouverte ferait suivre l'audience dans le vide jusqu'à la
|
|
2106
|
+
// péremption. On ne nettoie rien ici — la page disparaît de toute façon.
|
|
2107
|
+
window.addEventListener('pagehide',function(){
|
|
2108
|
+
if(!PRES) return;
|
|
2109
|
+
try{
|
|
2110
|
+
var b=JSON.stringify({action:'present-end',slug:PRES.slug,control:PRES.control});
|
|
2111
|
+
if(navigator.sendBeacon) navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));
|
|
2112
|
+
else fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});
|
|
2113
|
+
}catch(e){}
|
|
2114
|
+
});
|
|
2051
2115
|
// Transfert : je passe la main → je cesse de piloter SANS clôturer la présentation (le nouvel owner reprendra).
|
|
2052
2116
|
function stopPilotingLocally(){ if(!PRES)return; var s=PRES.slug; PRES=null; clearInterval(_hbIv); clearCtl(s); var pb=document.getElementById('pbar'); if(pb)pb.style.display='none'; try{ if(window.Live) Live.disconnect(); }catch(e){} }
|
|
2053
2117
|
function liveConnect(slug,control){ try{ if(window.Live){ var P=CFG.present||{}; Live.connect(slug,{name:P.by||'Présentateur',email:P.email||'',avatar:P.av||'',role:'presenter',member:true},control); } }catch(e){} }
|
|
@@ -2077,14 +2141,17 @@ ${LEGAL_CSS}
|
|
|
2077
2141
|
var tries=0; (function jump(){ var el=(document.getElementById('pages')||document).querySelector('.page[data-p="'+target+'"]'); if(el){ el.scrollIntoView({block:'start'}); } else if(tries++<40){ setTimeout(jump,150); } })();
|
|
2078
2142
|
}).catch(function(){});
|
|
2079
2143
|
}
|
|
2080
|
-
// Carte live : persiste le contenu (present-content
|
|
2144
|
+
// Carte live : persiste le contenu (present-content) → l'audience bascule/suit via Realtime.
|
|
2145
|
+
// On envoie le jeton de contrôle EN PLUS de la session : une présentation démarrée sans
|
|
2146
|
+
// session pilote sa carte comme elle tourne ses pages. Sans lui, l'appel repartait en 401,
|
|
2147
|
+
// avale par le .catch ci-dessous, et la carte ne suivait pas sans que rien ne le dise.
|
|
2081
2148
|
// ⚠️ REND SA PROMESSE, ET CE N'EST PAS DÉCORATIF. L'ordonnanceur des écritures de carte
|
|
2082
2149
|
// appelait fini() juste après l'appel, sans attendre : il croyait donc l'écriture terminée
|
|
2083
2150
|
// alors qu'elle venait de PARTIR. Sa garantie « une seule en vol, la dernière gagne » ne
|
|
2084
2151
|
// s'appliquait qu'à l'ordre des APPELS, pas à celui des écritures — plusieurs PATCH pouvaient
|
|
2085
2152
|
// voler ensemble, et une position ancienne atterrir après une récente.
|
|
2086
2153
|
function presentContent(content){ if(!PRES)return Promise.resolve(); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
|
|
2087
|
-
return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,content:content})})
|
|
2154
|
+
return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,control:PRES.control,content:content})})
|
|
2088
2155
|
.then(function(r){ if(r&&r.ok)diffuserEtat(); })
|
|
2089
2156
|
.catch(function(){}); }
|
|
2090
2157
|
function showMap(){ if(!PRES||!window.Map3DD)return; var wrap=document.getElementById('mapWrap'); if(wrap&&wrap.classList.contains('on')){ Map3DD.enter(null,true,presentContent); return; } var init=Player.presentation.initialMapContent(); presentContent(init); Map3DD.enter(init,true,presentContent); }
|
|
@@ -2798,8 +2865,28 @@ async function handler(req, res) {
|
|
|
2798
2865
|
// ⚠️ Une identité prouvée REMPLACE celle qu'on affirme — elle ne s'y ajoute pas. Sinon la
|
|
2799
2866
|
// vérification ne servirait qu'à décorer une affirmation qu'on croit toujours.
|
|
2800
2867
|
const profil = await profilDuJeton(req);
|
|
2868
|
+
// ⚠️ ET LA CLÉ EN FAIT PARTIE — C'EST MÊME LA SEULE QUI COMPTE. Le correctif ci-dessus a
|
|
2869
|
+
// remplacé le nom, l'e-mail et l'avatar par ceux du jeton, et laissé passer `key`, qui est
|
|
2870
|
+
// pourtant l'IDENTITÉ DE LA LIGNE. Or pour un membre cette clé EST son e-mail : un
|
|
2871
|
+
// participant anonyme n'avait qu'à poster la vôtre pour écraser votre ligne — nom, avatar —
|
|
2872
|
+
// et gonfler votre temps de présence. Ce sont les statistiques de présentation, c'est-à-dire
|
|
2873
|
+
// ce que ce produit vend.
|
|
2874
|
+
//
|
|
2875
|
+
// On dérive donc la clé de ce qui est prouvé, et jamais de ce qui est affirmé. Un anonyme,
|
|
2876
|
+
// lui, ne peut rien prouver : sa clé reste la sienne, mais enfermée dans un espace de noms
|
|
2877
|
+
// dont elle ne peut pas sortir — elle ne pourra jamais ressembler à l'e-mail d'un membre.
|
|
2801
2878
|
const r = await recordAttendance(String(body.slug || ""), {
|
|
2802
|
-
|
|
2879
|
+
// ⚠️ MINUSCULÉE, ET CE DÉTAIL EST UNE LIGNE DE PRÉSENCE. La clé cliente que 0.1.42 a
|
|
2880
|
+
// remplacée l'était (`me.email.toLowerCase()`) ; la clé dérivée ne l'était pas, et la
|
|
2881
|
+
// ligne se retrouve par `attendee_key=eq.` — une correspondance EXACTE. Un hôte dont
|
|
2882
|
+
// l'identité rend l'adresse telle qu'elle a été saisie aurait donc vu, pour un même
|
|
2883
|
+
// membre, une SECONDE ligne apparaître : temps cumulé reparti de zéro, et le collègue
|
|
2884
|
+
// affiché deux fois dans la liste des participants.
|
|
2885
|
+
//
|
|
2886
|
+
// Sans effet là où les adresses sont déjà normalisées — c'est le cas des deux hôtes
|
|
2887
|
+
// actuels, et c'est pour ça que rien ne l'aurait signalé. Un contrat ouvert ne se
|
|
2888
|
+
// repose pas sur ce que ses deux premiers hôtes font.
|
|
2889
|
+
key: profil ? lcMembre(profil.email) : cleAnonyme(body.key),
|
|
2803
2890
|
name: (profil && profil.name) || body.name,
|
|
2804
2891
|
email: profil ? profil.email : body.email,
|
|
2805
2892
|
avatar: (profil && profil.avatar) || body.avatar,
|
|
@@ -2813,8 +2900,18 @@ async function handler(req, res) {
|
|
|
2813
2900
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2814
2901
|
try {
|
|
2815
2902
|
const u = await PLAYER.identity.verifyToken(req.headers.authorization);
|
|
2816
|
-
|
|
2817
|
-
|
|
2903
|
+
// ⚠️ UNE EXCEPTION, ET UNE SEULE. `present-content` pilote ce que la présentation AFFICHE,
|
|
2904
|
+
// exactement comme `present-page` tourne les pages — et `present-page` se contente du
|
|
2905
|
+
// `control_token`, sans session. Exiger un JWT ici cassait les présentations démarrées
|
|
2906
|
+
// sans session, que `present-start` autorise pourtant : la carte ne suivait pas, en
|
|
2907
|
+
// silence. Le jeton de contrôle est vérifié plus bas, contre la ligne de la présentation.
|
|
2908
|
+
//
|
|
2909
|
+
// ⚠️ Les autres actions de ce groupe restent réservées à une session, et `present-switch`
|
|
2910
|
+
// — changer le DOCUMENT montré — au propriétaire : décider ce qu'on montre n'est pas
|
|
2911
|
+
// piloter l'affichage.
|
|
2912
|
+
const pilotage = body.action === "present-content" && !!body.control;
|
|
2913
|
+
if ((!u || !u.email) && !pilotage) return jp(401, { ok: false, error: "auth" });
|
|
2914
|
+
const isAdmin = !!u && PLAYER.identity.isAdmin(u);
|
|
2818
2915
|
let r;
|
|
2819
2916
|
if (body.action === "present-list") r = { ok: true, presentations: await listActivePresentations(u.email) };
|
|
2820
2917
|
else if (body.action === "present-reclaim") r = await reclaimPresentation(String(body.slug || ""), u.email);
|
|
@@ -2825,7 +2922,7 @@ async function handler(req, res) {
|
|
|
2825
2922
|
if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
|
|
2826
2923
|
r = await switchPresentationDoc(String(body.slug || ""), u.email, isAdmin, { fileUrl: body.fileUrl, fileName: body.fileName, docTitle: body.docTitle, docId: body.docId });
|
|
2827
2924
|
}
|
|
2828
|
-
else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), u.email, isAdmin, body.content);
|
|
2925
|
+
else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), (u && u.email) || "", isAdmin, body.content, String(body.control || ""));
|
|
2829
2926
|
else r = await handoverPresentation(String(body.slug || ""), u.email, body.newOwner);
|
|
2830
2927
|
return jp(r.ok ? 200 : (r.status || 400), r);
|
|
2831
2928
|
} catch { return jp(500, { ok: false }); }
|
|
@@ -3295,6 +3392,36 @@ async function handler(req, res) {
|
|
|
3295
3392
|
// Mode « Présenter » côté AUDIENCE : `?present=<slug>` → page live (suit le présentateur via Realtime) ;
|
|
3296
3393
|
// `&file=1` → stream le PDF de la présentation (Range, même origine pour pdf.js).
|
|
3297
3394
|
if (q.present) {
|
|
3395
|
+
// ⚠️ LA GARDE PASSE DEVANT L'INTERROGATION DE BASE, PAS DERRIÈRE.
|
|
3396
|
+
//
|
|
3397
|
+
// `state=1` et `chat=1` sont servis sans session, sur un lien public, et chaque appel coûte
|
|
3398
|
+
// une requête base. Une boucle sur une URL connue faisait donc d'un lien de présentation un
|
|
3399
|
+
// amplificateur : une requête HTTP triviale contre une requête base, autant de fois qu'on
|
|
3400
|
+
// veut. Douze actions d'écriture étaient limitées ; ces deux LECTURES ne l'étaient pas.
|
|
3401
|
+
//
|
|
3402
|
+
// ⚠️ Et c'est la ressource PARTAGÉE qui paie : la base est la même pour tous les documents de
|
|
3403
|
+
// l'instance. Le coût d'un abus ne retombe pas sur celui qui le commet.
|
|
3404
|
+
//
|
|
3405
|
+
// Placée AVANT `getPresentation` — écrite après, elle aurait laissé passer très exactement la
|
|
3406
|
+
// requête qu'elle est censée épargner. Le quota se déduit de la cadence de l'audience
|
|
3407
|
+
// (`src/cadence.ts`), il n'est pas choisi à la main.
|
|
3408
|
+
const sondage = String(q.state || "") === "1" || String(q.chat || "") === "1";
|
|
3409
|
+
if (sondage) {
|
|
3410
|
+
const ipSondage = adresseAppelant(req) || "anon";
|
|
3411
|
+
if (!(await PLAYER.limits.allow(`pread:${ipSondage}`, PRESENT_QUOTA_PER_HOUR, 3600))) {
|
|
3412
|
+
// ⚠️ UN REFUS MUET FAIT UNE AUDIENCE QUI DÉCROCHE SANS CAUSE NOMMÉE. Le 429 n'apparaît
|
|
3413
|
+
// que dans la console du spectateur ; l'exploitant, lui, verrait des pages qui ne tournent
|
|
3414
|
+
// plus chez tout un bâtiment. Une fois par heure suffit à nommer la cause sans inonder.
|
|
3415
|
+
if (await PLAYER.limits.allow("pread:quota-avert", 1, 3600)) {
|
|
3416
|
+
try {
|
|
3417
|
+
console.warn(`[player] relectures de présentation refusées : quota horaire par adresse atteint (${PRESENT_QUOTA_PER_HOUR}/h). Une audience nombreuse derrière une sortie unique verra ses pages cesser de tourner.`);
|
|
3418
|
+
} catch { /* sans console */ }
|
|
3419
|
+
}
|
|
3420
|
+
res.statusCode = 429; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
|
|
3421
|
+
res.end(JSON.stringify({ ok: false, error: "rate" }));
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3298
3425
|
const pres = await getPresentation(String(q.present));
|
|
3299
3426
|
if (!pres) return sendRefusal(res, "ended", embed);
|
|
3300
3427
|
// Historique du chat (chargé au join par présentateur ET audience).
|
package/server/presentations.js
CHANGED
|
@@ -364,11 +364,32 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
|
|
|
364
364
|
// finissaient par diverger, et une audience qui ne voit pas la bonne carte n'émet aucune erreur.
|
|
365
365
|
const { sanitizeContent } = require("./shared.generated.js");
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Ce que la présentation AFFICHE : carte, Street View, ou rien.
|
|
369
|
+
*
|
|
370
|
+
* ⚠️ DEUX AUTORITÉS, ET C'EST LE CORRECTIF. Cette fonction n'acceptait que le PROPRIÉTAIRE, alors
|
|
371
|
+
* que `setPage` — qui pilote exactement de la même façon — accepte le `control_token`. Conséquence :
|
|
372
|
+
* une présentation démarrée SANS session (c'est permis, `present-start` ne l'exige pas) pouvait
|
|
373
|
+
* tourner les pages mais pas déplacer sa carte. L'appel repartait en 401, avalé côté navigateur, et
|
|
374
|
+
* le présentateur voyait sa carte ne pas suivre sans qu'aucun message ne le dise.
|
|
375
|
+
*
|
|
376
|
+
* Piloter ce qui s'affiche et tourner une page sont le même acte : le `control_token` suffit aux
|
|
377
|
+
* deux. Le propriétaire garde son chemin — il pilote sa présentation depuis l'application sans
|
|
378
|
+
* détenir le jeton de contrôle du navigateur qui la présente.
|
|
379
|
+
*
|
|
380
|
+
* ⚠️ Ce qui reste réservé au propriétaire est `switchPresentationDoc` : changer le DOCUMENT n'est
|
|
381
|
+
* pas piloter l'affichage, c'est décider ce qui est montré. Les deux étaient groupés par voisinage
|
|
382
|
+
* dans la route, pas par autorité.
|
|
383
|
+
*
|
|
384
|
+
* Signalé par un audit externe : « le comportement actuel est ambigu ».
|
|
385
|
+
*/
|
|
386
|
+
async function setPresentationContent(slug, email, isAdmin, content, control) {
|
|
368
387
|
if (!slug) return { ok: false, status: 400 };
|
|
369
388
|
const row = await getPresentation(slug);
|
|
370
389
|
if (!row) return { ok: false, status: 404 };
|
|
371
|
-
|
|
390
|
+
const pilote = control && tokenMatches(control, row.control_hash);
|
|
391
|
+
const proprietaire = isAdmin || (row.owner_email && row.owner_email === lc(email));
|
|
392
|
+
if (!pilote && !proprietaire) return { ok: false, status: 403 };
|
|
372
393
|
await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { content: sanitizeContent(content), active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString() } });
|
|
373
394
|
return { ok: true };
|
|
374
395
|
}
|
|
@@ -28,7 +28,12 @@ __export(shared_exports, {
|
|
|
28
28
|
DEFAULT_CENTER: () => DEFAULT_CENTER,
|
|
29
29
|
DEFAULT_ZOOM: () => DEFAULT_ZOOM,
|
|
30
30
|
MAP_TYPES: () => MAP_TYPES,
|
|
31
|
+
PRESENTER_ACTIONS_PER_HOUR: () => PRESENTER_ACTIONS_PER_HOUR,
|
|
32
|
+
PRESENT_QUOTA_PER_HOUR: () => PRESENT_QUOTA_PER_HOUR,
|
|
33
|
+
PRESENT_READS_PER_HOUR: () => PRESENT_READS_PER_HOUR,
|
|
34
|
+
PRESENT_RESYNC_MS: () => PRESENT_RESYNC_MS,
|
|
31
35
|
READERS_PER_EGRESS: () => READERS_PER_EGRESS,
|
|
36
|
+
RESYNC_READS_PER_HOUR: () => RESYNC_READS_PER_HOUR,
|
|
32
37
|
SESSION_IDLE_MS: () => SESSION_IDLE_MS,
|
|
33
38
|
SESSION_INTERVAL_MS: () => SESSION_INTERVAL_MS,
|
|
34
39
|
SESSION_QUOTA_PER_HOUR: () => SESSION_QUOTA_PER_HOUR,
|
|
@@ -95,12 +100,22 @@ var SESSION_WRITES_PER_HOUR = Math.ceil(36e5 / SESSION_INTERVAL_MS);
|
|
|
95
100
|
var READERS_PER_EGRESS = 25;
|
|
96
101
|
var SESSION_QUOTA_PER_HOUR = SESSION_WRITES_PER_HOUR * READERS_PER_EGRESS;
|
|
97
102
|
var SESSION_IDLE_MS = 18e4;
|
|
103
|
+
var PRESENT_RESYNC_MS = 25e3;
|
|
104
|
+
var RESYNC_READS_PER_HOUR = Math.ceil(36e5 / PRESENT_RESYNC_MS);
|
|
105
|
+
var PRESENTER_ACTIONS_PER_HOUR = 720;
|
|
106
|
+
var PRESENT_READS_PER_HOUR = RESYNC_READS_PER_HOUR + PRESENTER_ACTIONS_PER_HOUR;
|
|
107
|
+
var PRESENT_QUOTA_PER_HOUR = PRESENT_READS_PER_HOUR * READERS_PER_EGRESS;
|
|
98
108
|
// Annotate the CommonJS export names for ESM import in node:
|
|
99
109
|
0 && (module.exports = {
|
|
100
110
|
DEFAULT_CENTER,
|
|
101
111
|
DEFAULT_ZOOM,
|
|
102
112
|
MAP_TYPES,
|
|
113
|
+
PRESENTER_ACTIONS_PER_HOUR,
|
|
114
|
+
PRESENT_QUOTA_PER_HOUR,
|
|
115
|
+
PRESENT_READS_PER_HOUR,
|
|
116
|
+
PRESENT_RESYNC_MS,
|
|
103
117
|
READERS_PER_EGRESS,
|
|
118
|
+
RESYNC_READS_PER_HOUR,
|
|
104
119
|
SESSION_IDLE_MS,
|
|
105
120
|
SESSION_INTERVAL_MS,
|
|
106
121
|
SESSION_QUOTA_PER_HOUR,
|