• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות

    מודל שמיעת וניהול המאזינים דרך הטלפון

    מתוזמן נעוץ נעול הועבר הסברים מסודרים ממשתמשים
    1 פוסטים 1 כותבים 27 צפיות 1 עוקבים
    טוען פוסטים נוספים
    • מהישן לחדש
    • מהחדש לישן
    • הכי הרבה הצבעות
    תגובה
    • תגובה כנושא
    התחברו כדי לפרסם תגובה
    נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
    • ה מחובר
      הדיבל המעופף
      נערך לאחרונה על ידי הדיבל המעופף

      זה הקוד לשלוחה

      type=api
      api_link=https://listener-management.a775529301.workers.dev
      api_add_0=token=טוקן
      api_url_post=yes
      

      זה קוד השרת למעונינים

      /* yemot-listeners — ממשק טלפוני לניהול מאזינים בקו.
       * מאפשר: לשמוע מי מאזין כרגע לקו (שם אם קיים, אחרת מספר, ובאיזו שלוחה הוא
       * נמצא), לנתק מאזין, או להעביר אותו לשלוחה אחרת — הכל בשיחה אחת, עד
       * שהמנהל מנתק את עצמו.
       *
       * סטטלס לגמרי: אין KV, אין Durable Object, אין שום Binding. אין שום דבר
       * להגדיר מלבד להדביק את הקוד ולפרוס.
       *
       * איך זה עובד בלי אחסון:
       * ימות שולח, בכל בקשה חוזרת בתוך אותה שיחה, את כל מה שנענה עד עכשיו יחד עם
       * הבקשה. לכן, כששואלים "לאיזה מאזין?" ואז "לאיזו שלוחה?", במקום לשמור
       * באיזה מאזין בחרו — מקודדים את הזיהוי שלו (id, האם חדר ועידה, טלפון, שם)
       * בתוך *שם השדה* של השאלה השנייה, וימות מחזיר אותו בדיוק כפי שהוא. חזרה
       * לתפריט אחרי טיפול במאזין נעשית עם go_to_folder לאותה שלוחה, שמתחיל
       * "סבב API" חדש ונקי, בדיוק כמו כניסה ראשונה.
       *
       * שם מאזין: לפי תיעוד ימות, callerIdNum כולל את ה-val_name (שם משויך
       * למספר) במידה וקיים במערכת וקיים login_add_val_name=yes. אם יש שם, הוא
       * יישמע במקום המספר. אם אצלך התכונה הזו לא מופעלת, פשוט תמיד יישמע המספר.
       *
       * שלוחה נוכחית: שדה path שמוחזר מ-GetIncomingCalls (מספר שלוחה + תיאור אם
       * יש). מוקרא כטקסט (t-) כי הוא לא בהכרח מספר "נקי" (יכול לכלול / ותיאור).
       *
       * אם ידוע לך באיזו שלוחה מוגדר הסקריפט הזה (type=api), אפשר לקבע כאן את
       * הנתיב (בלי סלאש בהתחלה, למשל "9" או "5/8") כדי לחזור אליה נקי בסיום כל
       * פעולה. אם משאירים ריק, ננסה את הפרמטר ApiExtension שהמערכת שולחת
       * אוטומטית בכל בקשה (יש לוודא ש-api_extension_send לא הוגדר no).
       */
      const SELF_EXTENSION_PATH = ""; // לדוגמה: "9"
      
      function freshSuffix() {
        return `${Date.now()}_${Math.floor(Math.random() * 1e4)}`;
      }
      
      function toHex(str) {
        const bytes = new TextEncoder().encode(str);
        let hex = "";
        for (const b of bytes) hex += b.toString(16).padStart(2, "0");
        return hex;
      }
      
      function fromHex(hex) {
        const bytes = new Uint8Array((hex || "").length / 2);
        for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
        return new TextDecoder().decode(bytes);
      }
      
      // מסיר תווים שמשמשים כתוחמים בתחביר של ימות (t-/d-/./=/&) כדי שטקסט חופשי
      // (שם מאזין, תיאור שלוחה) לא ישבור את ההודעה.
      function sanitizeSpeech(s) {
        return String(s || "").replace(/[.,\-=&]/g, " ").replace(/\s+/g, " ").trim();
      }
      
      // מפריד את מספר הטלפון מתוך callerIdNum מהשם המשויך (val_name) אם יש —
      // למשל "0533165488(זיהוי מוטי קרמר)". שומר על רווחים בין מילות השם (כדי
      // לא לדבק "מוטי" ו"קרמר" למילה אחת), ומוריד את תווית "זיהוי" שבתחילתו.
      function extractPhoneAndName(raw) {
        const s = String(raw || "");
        const phone = s.replace(/\D/g, "").slice(0, 10);
        let name = s
          .replace(/\d+/g, " ")
          .replace(/[()+\-:]/g, " ")
          .replace(/\s+/g, " ")
          .trim();
        name = name.replace(/^זיהוי\s*/, "").trim();
        return { phone, name: sanitizeSpeech(name) };
      }
      
      function yemot(body, status = 200) {
        return new Response(body, {
          status,
          headers: { "Content-Type": "text/plain; charset=utf-8" },
        });
      }
      
      async function parseParams(request) {
        const p = new URL(request.url).searchParams;
        if (request.method === "POST") {
          const ct = request.headers.get("Content-Type") || "";
          try {
            if (ct.includes("application/json")) {
              const json = await request.json();
              for (const [k, v] of Object.entries(json || {})) if (!p.has(k)) p.set(k, v);
            } else {
              const form = await request.formData();
              for (const [k, v] of form.entries()) if (!p.has(k)) p.set(k, v);
            }
          } catch (_) {}
        }
        return p;
      }
      
      const YM_API_URL = "https://www.call2all.co.il/ym/api/";
      
      async function ym(token, cmd, params) {
        const res = await fetch(`${YM_API_URL}${cmd}`, {
          method: "POST",
          headers: { "Authorization": token, "Content-Type": "application/json" },
          body: JSON.stringify(params),
        });
        return { status: res.status, data: await res.json().catch(() => ({})) };
      }
      
      function extractListeners(calls) {
        const seen = new Set();
        const out = [];
        for (const call of calls || []) {
          const push = (id, callerIdNum, conf, path) => {
            if (!id || seen.has(id)) return;
            seen.add(id);
            const { phone, name } = extractPhoneAndName(callerIdNum);
            out.push({ id, phone, name, conf, path: sanitizeSpeech(path || "") });
          };
      
          const confList = Array.isArray(call.confCalls)
            ? call.confCalls
            : call.confCalls && typeof call.confCalls === "object"
            ? Object.values(call.confCalls)
            : null;
      
          if (confList) {
            for (const c of confList) {
              if (c && typeof c === "object" && c.id) push(c.id, c.callerIdNum, true, c.path || call.path);
            }
          } else {
            push(call.id, call.callerIdNum, false, call.path);
          }
        }
        return out;
      }
      
      // "t-מספרו.d-...." או "t-לא ידוע." אם אין בכלל מספר
      function whoPhrase(phone, name) {
        if (name) return `t-בשם.t-${name}.`;
        if (phone) return `t-מספרו.d-${phone}.`;
        return `t-מספר לא ידוע.`;
      }
      
      function selfReentryDirective(apiExtension) {
        const raw = SELF_EXTENSION_PATH || apiExtension || "";
        const ext = String(raw).replace(/^\/+/, "");
        return ext ? `g-/${ext}` : null;
      }
      
      /* שלב א: שליפת המאזינים + השמעת הרשימה (שם/מספר + שלוחה נוכחית) + בקשת בחירה */
      async function showMenu(token, prefix = "") {
        const { data } = await ym(token, "GetIncomingCalls", {});
        if (data.responseStatus !== "OK")
          return yemot("id_list_message=t-שגיאה בקבלת השיחות הפעילות מהמערכת");
      
        const listeners = extractListeners(data.calls);
        if (!listeners.length)
          return yemot(`id_list_message=${prefix}t-אין מאזינים בקו כרגע`);
      
        const shown = listeners.slice(0, 9);
        const hiddenCount = listeners.length - shown.length;
        const field = `Choice_${freshSuffix()}`;
      
        const menu = shown
          .map((l, i) => {
            const where = l.path ? `t-נמצא בשלוחה.t-${l.path}.` : "";
            return `t-מאזין.d-${i + 1}.${whoPhrase(l.phone, l.name)}${where}`;
          })
          .join("");
      
        const hiddenNote =
          hiddenCount > 0 ? `t-מוצגים תשעת הראשונים מתוך.d-${listeners.length}.` : "";
      
        const message =
          `${prefix}t-מספר המאזינים בקו הוא.d-${shown.length}.${hiddenNote}${menu}` +
          `t-להעברת מאזין הקישו את מספרו`;
      
        // read=<הודעה>=<שם>,<שימוש בערך קיים>,<מקס'>,<מינ'>,<שניות המתנה>,<אופן השמעה>
        return yemot(`read=${message}=${field},,1,1,,Digits`);
      }
      
      /* שלב ב: בקשת שלוחת יעד (או 0 לניתוק). כל המידע על המאזין הנבחר מקודד
         בתוך שם השדה עצמו, כדי שיחזור אלינו בבקשה הבאה בלי שנצטרך לשמור אותו. */
      function promptDest(id, conf, phone, name, prefix = "", choiceDigit = null) {
        const payload = `${toHex(id)}_${conf ? 1 : 0}_${phone || ""}_${toHex(name || "")}`;
        const field = `Dest_${payload}_${freshSuffix()}`;
      
        const who = choiceDigit
          ? `t-בחרתם במאזין.d-${choiceDigit}.${whoPhrase(phone, name)}`
          : `t-המאזין הנבחר.${whoPhrase(phone, name)}`;
      
        const msg =
          `${prefix}${who}t-הקישו את מספר השלוחה להעברה, לשלוחה מקוננת הקישו כוכבית במקום סלאש, ` +
          `לניתוק המאזין הקישו אפס, וסולמית לסיום`;
      
        // read=<הודעה>=<שם>,<שימוש בערך קיים>,<מקס'>,<מינ'>,<שניות המתנה>,<אופן השמעה>,
        //      <חסימת כוכבית>,<חסימת אפס>,<החלפת מקש> — "*/ " מחליף כוכבית בסלאש,
        //      כדי לאפשר להקיש שלוחה מקוננת כמו 5/8 (בטלפון אי אפשר להקיש סלאש עצמו).
        return yemot(`read=${msg}=${field},,10,1,15,Digits,,,*/`);
      }
      
      export default {
        async fetch(request, env) {
          try {
            return await handle(request, env);
          } catch (e) {
            const msg = String(e && e.message ? e.message : e).slice(0, 50).replace(/[.,\-=&]/g, " ");
            return yemot(`id_list_message=t-שגיאה בשרת.t-${msg}`);
          }
        },
      };
      
      async function handle(request, env) {
        if (!(request.headers.get("User-Agent") || "").startsWith("yemot-core-api"))
          return yemot("id_list_message=t-גישה נדחתה", 403);
      
        const p = await parseParams(request);
      
        // דיבוג (אופציונלי, לא דורש שום הגדרה): ?token=...&debug=yes[&secret=...]
        if (p.get("debug") === "yes") {
          if (env && env.DEBUG_SECRET && p.get("secret") !== env.DEBUG_SECRET)
            return yemot("id_list_message=t-גישה נדחתה", 403);
          const { status, data } = await ym(p.get("token") || "", "GetIncomingCalls", {});
          return new Response(JSON.stringify({ httpStatus: status, response: data }, null, 2), {
            headers: { "Content-Type": "application/json; charset=utf-8" },
          });
        }
      
        if (p.get("hangup") === "yes") return new Response("OK");
      
        const token = p.get("token") || "";
        if (!/^\d{7,}:[A-Za-z0-9]+$/.test(token))
          return yemot("id_list_message=t-שגיאת הזדהות. הטוקן חסר או לא תקין");
      
        const apiExtension = p.get("ApiExtension") || "";
      
        // שלב ג: התקבל מענה לבקשת שלוחת היעד (או 0 לניתוק).
        // המידע על המאזין (id, conf, phone, name) יושב בתוך שם השדה עצמו.
        const destKey = [...p.keys()].find((k) => k.startsWith("Dest_"));
        if (destKey) {
          const m = destKey.match(/^Dest_([0-9a-f]+)_([01])_(\d*)_([0-9a-f]*)_\d+_\d+$/);
          if (m) {
            const id = fromHex(m[1]);
            const conf = m[2] === "1";
            const phone = m[3];
            const name = fromHex(m[4]);
            const dest = p.get(destKey);
      
            // כוכבית שהוקשה הומרה כבר לסלאש ע"י ימות (ראה הגדרת read למעלה),
            // אז נתיב מקונן מגיע כאן כבר בתור "5/8" וכו'.
            if (!/^\d{1,10}(\/\d{1,10})*$/.test(dest)) {
              return promptDest(id, conf, phone, name, "t-שלוחה לא תקינה.");
            }
      
            const isHangup = dest === "0";
            const go = isHangup ? "hangup" : `/${dest}`;
      
            if (conf) {
              const r1 = await ym(token, "CallAction", { ids: id, "set:ConfbridgeNewGoto": go });
              if (r1.data.responseStatus !== "OK")
                return yemot("id_list_message=t-שגיאה בכיוון היציאה מהחדר");
              const r2 = await ym(token, "CallAction", { ids: id, action: "kick" });
              if (r2.data.responseStatus !== "OK")
                return yemot("id_list_message=t-שגיאה בהוצאה מהחדר");
            } else {
              const r = await ym(token, "CallAction", { ids: id, action: `set:GOasap=${go}` });
              if (r.data.responseStatus !== "OK")
                return yemot("id_list_message=t-שגיאה בהעברת המאזין");
            }
      
            const successText = isHangup ? "t-המאזין נותק." : `t-המאזין הועבר לשלוחה.d-${dest}.`;
            const reentry = selfReentryDirective(apiExtension);
            // חזרה לתפריט מעודכן דרך go_to_folder (סבב API חדש ונקי) — לא דרך
            // read חוזר, כי read לא יכול לשמש שוב לאותם שמות שדות.
            return yemot(reentry ? `id_list_message=${successText}${reentry}` : `id_list_message=${successText}`);
          }
        }
      
        // שלב ב: התקבל מענה לבחירת מאזין (ספרה 1-9). שולפים רשימה עדכנית
        // ובוחרים ממנה לפי המיקום - אין שום צורך לשמור את הרשימה של שלב א'.
        const choiceKey = [...p.keys()].find((k) => k.startsWith("Choice_"));
        if (choiceKey) {
          const choice = p.get(choiceKey);
          const { data } = await ym(token, "GetIncomingCalls", {});
          if (data.responseStatus !== "OK")
            return yemot("id_list_message=t-שגיאה בקבלת השיחות הפעילות מהמערכת");
          const listeners = extractListeners(data.calls).slice(0, 9);
          const idx = parseInt(choice, 10) - 1;
          const rec = listeners[idx];
          if (!rec) return showMenu(token, "t-בחירה לא תקינה.");
          return promptDest(rec.id, rec.conf, rec.phone, rec.name, "", choice);
        }
      
        // שלב א: כניסה ראשונה לשלוחה (או חזרה אליה אחרי go_to_folder)
        return showMenu(token);
      }
      
      
      תגובה 1 תגובה אחרונה תגובה ציטוט 1

      שלום! נראה שהשיחה הזו מעניינת אותך, אבל עדיין אין לך חשבון.

      נמאס לכם לגלול בין אותם הפוסטים בכל ביקור? כשנרשמים לחשבון, תמיד תחזרו בדיוק למקום שבו הייתם קודם, ותוכלו לבחור לקבל התראות על תגובות חדשות (בין אם במייל, ובין אם בהתראת פוש). תוכלו גם לשמור סימניות ולפרגן ב-upvote לפוסטים כדי להביע הערכה לחברי קהילה אחרים.

      בעזרת התרומה שלך, הפוסט הזה יכול להיות אפילו טוב יותר 💗

      הרשמה התחברות
      • פוסט ראשון
        פוסט אחרון