discovery-media-player 0.1.40 → 0.1.42
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 +145 -19
- 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.42",
|
|
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 G=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)G(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&&G(e,o,{get:()=>n[o],enumerable:!(r=Me(n,o))||r.enumerable});return e};var _e=e=>Te(G({},\"__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:()=>B});var K=\"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 B(e){let n={type:K+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(K))return null;let o=r.slice(K.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(B(e),\"*\")}catch(t){}}function Re(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(B(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,k=r(),S=!1,U=!1,A=[],ve=()=>!!c||!!d,re=i=>{if(ve())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!S,oe=i=>{let l=r()-i,w=Math.max(0,k-i)+s;return Math.max(0,Math.min(l,w))},V=()=>{g!=null&&h>0&&(f[h]=(f[h]||0)+oe(g)/1e3),g=W()?r():null},F=()=>{V(),g=null},z=()=>{g==null&&W()&&(g=r())},j=()=>{let i=0;for(let l in f)i+=f[l];return g!=null&&(i+=oe(g)/1e3),Math.round(i)},ie=()=>{let i={};for(let l in f)i[l]=Math.round(f[l]);return i},E=()=>{k=r(),S&&(S=!1,z())},se=(i,l)=>{d||re({slug:c,event:i,page:l||M,maxPage:u,seconds:j(),sessionId:m})},R=()=>{V();let i={event:\"session\",sessionId:m,numPages:v,maxPage:u,totalSeconds:j(),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,re(i)},x=(i,l,w,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,w,ue),A.push(()=>{i.removeEventListener(l,w,ue)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>u,pageTimes:ie,start(){if(U)return;U=!0,se(\"open\",1),h=1,k=r(),g=W()?r():null,x(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(F(),R()):(E(),z())}),x(n,\"blur\",F),x(n,\"focus\",()=>{E(),z()});for(let w of De)x(n,w,E,{passive:!0});x(e.scrollElement,\"scroll\",E,{passive:!0}),x(n,\"beforeunload\",R);let i=n.setInterval(()=>{!S&&r()-k>s&&(S=!0,F())},5e3),l=n.setInterval(R,a);A.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(k=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:E,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:()=>Z,canModerate:()=>X,createScheduler:()=>Xe,escapeHtml:()=>p,flattenPresence:()=>Fe,formatMessageBody:()=>Y,initials:()=>me,isMentioned:()=>Ze,isMine:()=>T,reactorId:()=>q,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 Z(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(me(n))}function Y(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 q(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function T(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 X(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}`)&&!T(e,n):!1}function Ye(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(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=q(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=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||X(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>${Y(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${Z(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\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var N={};y(N,{DEFAULT_CENTER:()=>ee,DEFAULT_ZOOM:()=>ne,MAP_TYPES:()=>P,cycleMapType:()=>en,initialMapContent:()=>tn,mapTypeLabel:()=>nn,sanitizeContent:()=>te});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ee=[46.6,2.5],ne=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function J(e){if(!Array.isArray(e))return null;let n=_(e[0]),t=_(e[1]);return n!=null&&t!=null?[n,t]:null}var Q=(e,n,t)=>Math.max(n,Math.min(t,e));function te(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=J(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:Q(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:Q(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:J(n.center)||[...ee],zoom:Q(Math.trunc(_(n.zoom)||ne),1,21),marker:J(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:[...ee],zoom:ne,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=te(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: "5a5b02da1632ec1a" };
|
|
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
|
|
@@ -629,7 +629,13 @@ var Map3DD=(function(){
|
|
|
629
629
|
// prix pour que personne d'autre que le présentateur ne pilote l'écran de l'audience.
|
|
630
630
|
var _ordPersist=null;
|
|
631
631
|
function persistOrd(){ if(!_ordPersist&&window.Player&&Player.live&&Player.live.createScheduler){
|
|
632
|
-
_ordPersist=Player.live.createScheduler(function(fini){
|
|
632
|
+
_ordPersist=Player.live.createScheduler(function(fini){
|
|
633
|
+
// ⚠️ ON ATTEND L'ÉCRITURE. Appeler fini() tout de suite disait à l'ordonnanceur que le
|
|
634
|
+
// travail était fait alors qu'il commençait : sa garantie du « dernier état gagne »
|
|
635
|
+
// portait sur l'ordre des appels et non sur celui des arrivées.
|
|
636
|
+
var p=null; try{ if(map&&persist)p=persist(state()); }catch(e){}
|
|
637
|
+
if(p&&p.then)p.then(fini,fini); else fini();
|
|
638
|
+
},{minMs:500}); }
|
|
633
639
|
return _ordPersist; }
|
|
634
640
|
function broadcast(){ if(!map)return; var n=Date.now(); if(n-_bcT<200)return; _bcT=n;
|
|
635
641
|
var o=persistOrd(); if(o)o.signaler();
|
|
@@ -676,7 +682,10 @@ var Map3DD=(function(){
|
|
|
676
682
|
// Même règle qu'au-dessus : on persiste, on signale, on ne transporte pas la position.
|
|
677
683
|
var _ordSv=null;
|
|
678
684
|
function svOrd(){ if(!_ordSv&&window.Player&&Player.live&&Player.live.createScheduler){
|
|
679
|
-
_ordSv=Player.live.createScheduler(function(fini){
|
|
685
|
+
_ordSv=Player.live.createScheduler(function(fini){
|
|
686
|
+
var p=null; try{ if(persist&&pano)p=persist(svState()); }catch(e){}
|
|
687
|
+
if(p&&p.then)p.then(fini,fini); else fini();
|
|
688
|
+
},{minMs:500}); }
|
|
680
689
|
return _ordSv; }
|
|
681
690
|
function svBcast(){ var n=Date.now(); if(n-_svBcT<200)return; _svBcT=n; if(!svState())return;
|
|
682
691
|
var o=svOrd(); if(o)o.signaler();
|
|
@@ -1362,6 +1371,25 @@ const CLE_INVITE = "dmp-present-me";
|
|
|
1362
1371
|
*
|
|
1363
1372
|
* Signalé par la seconde passe d'audit (P1-6).
|
|
1364
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
|
+
function cleAnonyme(brut) {
|
|
1389
|
+
const v = String(brut == null ? "" : brut).trim().slice(0, 120);
|
|
1390
|
+
return /^anon-[A-Za-z0-9_-]{4,}$/.test(v) ? v : "anon-inconnu";
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1365
1393
|
async function profilDuJeton(req) {
|
|
1366
1394
|
try {
|
|
1367
1395
|
const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
|
|
@@ -2033,12 +2061,52 @@ ${LEGAL_CSS}
|
|
|
2033
2061
|
//
|
|
2034
2062
|
// sendBeacon ne s'attend pas, mais il rend la main une fois la requête MISE EN FILE : signaler
|
|
2035
2063
|
// juste après, puis couper, respecte l'ordre autant que ce transport le permet.
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2064
|
+
// FERMER UNE PRÉSENTATION EST UN ACTE, PAS UNE INTENTION.
|
|
2065
|
+
//
|
|
2066
|
+
// ⚠️ Cette fonction annonçait la fin avant de l'obtenir. « sendBeacon » ne rend AUCUNE réponse —
|
|
2067
|
+
// ni « enregistré » ni « refusé » — et pourtant l'interface se nettoyait, le jeton de contrôle
|
|
2068
|
+
// était effacé et le canal fermé dans la foulée. Si l'appel échouait, la présentation restait
|
|
2069
|
+
// VIVANTE pour l'audience pendant que le présentateur la croyait close ; et il n'avait même plus
|
|
2070
|
+
// de quoi la refermer, « clearCtl » ayant déjà jeté son jeton.
|
|
2071
|
+
//
|
|
2072
|
+
// ⚠️ Le beacon n'achetait rien ici : il sert à faire partir une requête pendant que la page
|
|
2073
|
+
// MEURT. Or le seul appelant était un bouton — on a tout le temps d'attendre. Il est désormais à
|
|
2074
|
+
// sa place, sur « pagehide », et là seulement.
|
|
2075
|
+
//
|
|
2076
|
+
// ⚠️ ET L'ORDRE COMPTE : l'audience ne relit l'état qu'APRÈS le 2xx. Diffuser avant, c'était lui
|
|
2077
|
+
// faire relire une présentation encore marquée active — elle rejouait donc l'ancien état.
|
|
2078
|
+
//
|
|
2079
|
+
// On n'efface donc rien tant que le serveur n'a pas confirmé : en cas d'échec le pilotage reste
|
|
2080
|
+
// entier et le présentateur peut réessayer. Une présentation vraiment abandonnée est rattrapée
|
|
2081
|
+
// par la péremption (STALE_MS, 3 min), qui existe exactement pour ce cas.
|
|
2082
|
+
//
|
|
2083
|
+
// Signalé par un audit externe.
|
|
2084
|
+
function endPresent(){
|
|
2085
|
+
if(!PRES) return Promise.resolve();
|
|
2086
|
+
var p=PRES, btn=document.getElementById('pbarEnd'), libelle=btn?btn.textContent:'';
|
|
2087
|
+
if(btn){ btn.disabled=true; btn.textContent='…'; }
|
|
2088
|
+
return fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,
|
|
2089
|
+
body:JSON.stringify({action:'present-end',slug:p.slug,control:p.control})})
|
|
2090
|
+
.then(function(r){ if(!r||!r.ok) throw new Error('present-end'); })
|
|
2091
|
+
.then(function(){
|
|
2092
|
+
PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
|
|
2093
|
+
var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
|
|
2094
|
+
try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){}
|
|
2095
|
+
}, function(){
|
|
2096
|
+
if(btn){ btn.disabled=false; btn.textContent=libelle||'Terminer'; btn.title='La fin n’a pas été enregistrée — réessayez.'; }
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
// ⚠️ LE SEUL ENDROIT OÙ « sendBeacon » A UN SENS : la page s'en va, aucune réponse ne pourra être
|
|
2100
|
+
// lue, et une présentation laissée ouverte ferait suivre l'audience dans le vide jusqu'à la
|
|
2101
|
+
// péremption. On ne nettoie rien ici — la page disparaît de toute façon.
|
|
2102
|
+
window.addEventListener('pagehide',function(){
|
|
2103
|
+
if(!PRES) return;
|
|
2104
|
+
try{
|
|
2105
|
+
var b=JSON.stringify({action:'present-end',slug:PRES.slug,control:PRES.control});
|
|
2106
|
+
if(navigator.sendBeacon) navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));
|
|
2107
|
+
else fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});
|
|
2108
|
+
}catch(e){}
|
|
2109
|
+
});
|
|
2042
2110
|
// Transfert : je passe la main → je cesse de piloter SANS clôturer la présentation (le nouvel owner reprendra).
|
|
2043
2111
|
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){} }
|
|
2044
2112
|
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){} }
|
|
@@ -2068,9 +2136,17 @@ ${LEGAL_CSS}
|
|
|
2068
2136
|
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); } })();
|
|
2069
2137
|
}).catch(function(){});
|
|
2070
2138
|
}
|
|
2071
|
-
// Carte live : persiste le contenu (present-content
|
|
2072
|
-
|
|
2073
|
-
|
|
2139
|
+
// Carte live : persiste le contenu (present-content) → l'audience bascule/suit via Realtime.
|
|
2140
|
+
// On envoie le jeton de contrôle EN PLUS de la session : une présentation démarrée sans
|
|
2141
|
+
// session pilote sa carte comme elle tourne ses pages. Sans lui, l'appel repartait en 401,
|
|
2142
|
+
// avale par le .catch ci-dessous, et la carte ne suivait pas sans que rien ne le dise.
|
|
2143
|
+
// ⚠️ REND SA PROMESSE, ET CE N'EST PAS DÉCORATIF. L'ordonnanceur des écritures de carte
|
|
2144
|
+
// appelait fini() juste après l'appel, sans attendre : il croyait donc l'écriture terminée
|
|
2145
|
+
// alors qu'elle venait de PARTIR. Sa garantie « une seule en vol, la dernière gagne » ne
|
|
2146
|
+
// s'appliquait qu'à l'ordre des APPELS, pas à celui des écritures — plusieurs PATCH pouvaient
|
|
2147
|
+
// voler ensemble, et une position ancienne atterrir après une récente.
|
|
2148
|
+
function presentContent(content){ if(!PRES)return Promise.resolve(); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
|
|
2149
|
+
return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,control:PRES.control,content:content})})
|
|
2074
2150
|
.then(function(r){ if(r&&r.ok)diffuserEtat(); })
|
|
2075
2151
|
.catch(function(){}); }
|
|
2076
2152
|
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); }
|
|
@@ -2784,8 +2860,18 @@ async function handler(req, res) {
|
|
|
2784
2860
|
// ⚠️ Une identité prouvée REMPLACE celle qu'on affirme — elle ne s'y ajoute pas. Sinon la
|
|
2785
2861
|
// vérification ne servirait qu'à décorer une affirmation qu'on croit toujours.
|
|
2786
2862
|
const profil = await profilDuJeton(req);
|
|
2863
|
+
// ⚠️ ET LA CLÉ EN FAIT PARTIE — C'EST MÊME LA SEULE QUI COMPTE. Le correctif ci-dessus a
|
|
2864
|
+
// remplacé le nom, l'e-mail et l'avatar par ceux du jeton, et laissé passer `key`, qui est
|
|
2865
|
+
// pourtant l'IDENTITÉ DE LA LIGNE. Or pour un membre cette clé EST son e-mail : un
|
|
2866
|
+
// participant anonyme n'avait qu'à poster la vôtre pour écraser votre ligne — nom, avatar —
|
|
2867
|
+
// et gonfler votre temps de présence. Ce sont les statistiques de présentation, c'est-à-dire
|
|
2868
|
+
// ce que ce produit vend.
|
|
2869
|
+
//
|
|
2870
|
+
// On dérive donc la clé de ce qui est prouvé, et jamais de ce qui est affirmé. Un anonyme,
|
|
2871
|
+
// lui, ne peut rien prouver : sa clé reste la sienne, mais enfermée dans un espace de noms
|
|
2872
|
+
// dont elle ne peut pas sortir — elle ne pourra jamais ressembler à l'e-mail d'un membre.
|
|
2787
2873
|
const r = await recordAttendance(String(body.slug || ""), {
|
|
2788
|
-
key: body.key,
|
|
2874
|
+
key: profil ? profil.email : cleAnonyme(body.key),
|
|
2789
2875
|
name: (profil && profil.name) || body.name,
|
|
2790
2876
|
email: profil ? profil.email : body.email,
|
|
2791
2877
|
avatar: (profil && profil.avatar) || body.avatar,
|
|
@@ -2799,8 +2885,18 @@ async function handler(req, res) {
|
|
|
2799
2885
|
const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
|
|
2800
2886
|
try {
|
|
2801
2887
|
const u = await PLAYER.identity.verifyToken(req.headers.authorization);
|
|
2802
|
-
|
|
2803
|
-
|
|
2888
|
+
// ⚠️ UNE EXCEPTION, ET UNE SEULE. `present-content` pilote ce que la présentation AFFICHE,
|
|
2889
|
+
// exactement comme `present-page` tourne les pages — et `present-page` se contente du
|
|
2890
|
+
// `control_token`, sans session. Exiger un JWT ici cassait les présentations démarrées
|
|
2891
|
+
// sans session, que `present-start` autorise pourtant : la carte ne suivait pas, en
|
|
2892
|
+
// silence. Le jeton de contrôle est vérifié plus bas, contre la ligne de la présentation.
|
|
2893
|
+
//
|
|
2894
|
+
// ⚠️ Les autres actions de ce groupe restent réservées à une session, et `present-switch`
|
|
2895
|
+
// — changer le DOCUMENT montré — au propriétaire : décider ce qu'on montre n'est pas
|
|
2896
|
+
// piloter l'affichage.
|
|
2897
|
+
const pilotage = body.action === "present-content" && !!body.control;
|
|
2898
|
+
if ((!u || !u.email) && !pilotage) return jp(401, { ok: false, error: "auth" });
|
|
2899
|
+
const isAdmin = !!u && PLAYER.identity.isAdmin(u);
|
|
2804
2900
|
let r;
|
|
2805
2901
|
if (body.action === "present-list") r = { ok: true, presentations: await listActivePresentations(u.email) };
|
|
2806
2902
|
else if (body.action === "present-reclaim") r = await reclaimPresentation(String(body.slug || ""), u.email);
|
|
@@ -2811,7 +2907,7 @@ async function handler(req, res) {
|
|
|
2811
2907
|
if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
|
|
2812
2908
|
r = await switchPresentationDoc(String(body.slug || ""), u.email, isAdmin, { fileUrl: body.fileUrl, fileName: body.fileName, docTitle: body.docTitle, docId: body.docId });
|
|
2813
2909
|
}
|
|
2814
|
-
else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), u.email, isAdmin, body.content);
|
|
2910
|
+
else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), (u && u.email) || "", isAdmin, body.content, String(body.control || ""));
|
|
2815
2911
|
else r = await handoverPresentation(String(body.slug || ""), u.email, body.newOwner);
|
|
2816
2912
|
return jp(r.ok ? 200 : (r.status || 400), r);
|
|
2817
2913
|
} catch { return jp(500, { ok: false }); }
|
|
@@ -3281,6 +3377,36 @@ async function handler(req, res) {
|
|
|
3281
3377
|
// Mode « Présenter » côté AUDIENCE : `?present=<slug>` → page live (suit le présentateur via Realtime) ;
|
|
3282
3378
|
// `&file=1` → stream le PDF de la présentation (Range, même origine pour pdf.js).
|
|
3283
3379
|
if (q.present) {
|
|
3380
|
+
// ⚠️ LA GARDE PASSE DEVANT L'INTERROGATION DE BASE, PAS DERRIÈRE.
|
|
3381
|
+
//
|
|
3382
|
+
// `state=1` et `chat=1` sont servis sans session, sur un lien public, et chaque appel coûte
|
|
3383
|
+
// une requête base. Une boucle sur une URL connue faisait donc d'un lien de présentation un
|
|
3384
|
+
// amplificateur : une requête HTTP triviale contre une requête base, autant de fois qu'on
|
|
3385
|
+
// veut. Douze actions d'écriture étaient limitées ; ces deux LECTURES ne l'étaient pas.
|
|
3386
|
+
//
|
|
3387
|
+
// ⚠️ Et c'est la ressource PARTAGÉE qui paie : la base est la même pour tous les documents de
|
|
3388
|
+
// l'instance. Le coût d'un abus ne retombe pas sur celui qui le commet.
|
|
3389
|
+
//
|
|
3390
|
+
// Placée AVANT `getPresentation` — écrite après, elle aurait laissé passer très exactement la
|
|
3391
|
+
// requête qu'elle est censée épargner. Le quota se déduit de la cadence de l'audience
|
|
3392
|
+
// (`src/cadence.ts`), il n'est pas choisi à la main.
|
|
3393
|
+
const sondage = String(q.state || "") === "1" || String(q.chat || "") === "1";
|
|
3394
|
+
if (sondage) {
|
|
3395
|
+
const ipSondage = adresseAppelant(req) || "anon";
|
|
3396
|
+
if (!(await PLAYER.limits.allow(`pread:${ipSondage}`, PRESENT_QUOTA_PER_HOUR, 3600))) {
|
|
3397
|
+
// ⚠️ UN REFUS MUET FAIT UNE AUDIENCE QUI DÉCROCHE SANS CAUSE NOMMÉE. Le 429 n'apparaît
|
|
3398
|
+
// que dans la console du spectateur ; l'exploitant, lui, verrait des pages qui ne tournent
|
|
3399
|
+
// plus chez tout un bâtiment. Une fois par heure suffit à nommer la cause sans inonder.
|
|
3400
|
+
if (await PLAYER.limits.allow("pread:quota-avert", 1, 3600)) {
|
|
3401
|
+
try {
|
|
3402
|
+
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.`);
|
|
3403
|
+
} catch { /* sans console */ }
|
|
3404
|
+
}
|
|
3405
|
+
res.statusCode = 429; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
|
|
3406
|
+
res.end(JSON.stringify({ ok: false, error: "rate" }));
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3284
3410
|
const pres = await getPresentation(String(q.present));
|
|
3285
3411
|
if (!pres) return sendRefusal(res, "ended", embed);
|
|
3286
3412
|
// 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,
|