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

    שלוחת סקר - קלה ופשוטה לשימוש !!!

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

      קוד השרת שודרג !!! כמו"כ ממשק ניהול למחשב גללו למטה ותראו

      שים לב! בשביל השלוחה צריך שרת עריכה: מי שאין לו שיסתכל למטה

      אפשר לנסות בקו 0799256584 שלוחה 55

      קוד השלוחה

      title=סקר לדוגמא
      type=api
      api_link=כתובת השרת
      api_add_0=token=טוקן
      

      בשלוחת הסקר יש ליצור קובץ בשם Surveyquestion.ini בו נגדיר את השאלות

      שאלה
      question=האם אתם הולכים לישון מוקדם אם כן הקישו אחד אם לא הקישו שתיים
      תשובות
      possibility1=כן
      possibility2=לא
      
      

      הקוד לשרת

      /**
       * survey-worker.js
       * -----------------
       * שלוחת סקר לימות המשיח (מודול API, type=api) + נקודת קצה לדשבורד ניהול.
       *
       * שני קבצים נשמרים בתיקיית השלוחה (ivr2), דרך ה-API הרגיל למפתחים
       * (הפקודות GetTextFile / UploadTextFile):
       *
       *   Surveyquestion.ini   - מוגדר ידנית מראש, מכיל את השאלה והאפשרויות.
       *   Surveydata.ini       - נכתב אוטומטית על ידי ה-Worker, שורה לכל מצביע:
       *                          <טלפון>,<מספר_אפשרות>,<תאריך_ISO>
       *
       * הגדרות נדרשות ב-ext.ini של השלוחה:
       *
       *   type=api
       *   api_link=https://YOUR-WORKER.workers.dev/survey
       *   api_add_0=token=YOUR_YEMOT_DEV_API_TOKEN
       *
       * (ה-token הוא טוקן ה-API הקבוע של ימות המשיח - "חומת האש" - המשמש
       *  את ה-Worker לקרוא ולכתוב את קבצי ה-ini באמצעות ה-API הרגיל למפתחים)
       */
      
      const YEMOT_API_BASE = "https://www.call2all.co.il/ym/api/";
      
      // ---------- עזרים כלליים ----------
      
      function corsHeaders() {
        return {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
          "Access-Control-Allow-Headers": "Content-Type",
        };
      }
      
      function textResponse(body) {
        return new Response(body, {
          status: 200,
          headers: { "Content-Type": "text/plain; charset=utf-8" },
        });
      }
      
      function jsonResponse(obj, status = 200) {
        return new Response(JSON.stringify(obj), {
          status,
          headers: {
            "Content-Type": "application/json; charset=utf-8",
            ...corsHeaders(),
          },
        });
      }
      
      // מנקה תווים שימות המשיח לא מאפשר בטקסט להשמעה (t-) ותווים
      // שעלולים לשבש את תחביר תשובת השרת (& = )
      function sanitizeText(s) {
        return (s || "")
          .replace(/[.\-&=]/g, " ")
          .replace(/\s+/g, " ")
          .trim();
      }
      
      // פרסור קובץ ini פשוט (key=value בכל שורה) - עבור Surveyquestion.ini
      function parseIni(text) {
        const obj = {};
        if (!text) return obj;
        const lines = text.split(/\r?\n/);
        for (const raw of lines) {
          const line = raw.trim();
          if (!line || line.startsWith(";") || line.startsWith("#")) continue;
          const idx = line.indexOf("=");
          if (idx === -1) continue;
          const key = line.slice(0, idx).trim();
          const val = line.slice(idx + 1).trim();
          obj[key] = val;
        }
        return obj;
      }
      
      // פרסור Surveydata.ini - שורה לכל מצביע: phone,choice,isoTime
      function parseSurveyData(text) {
        const votes = [];
        if (!text) return votes;
        const lines = text.split(/\r?\n/);
        for (const raw of lines) {
          const line = raw.trim();
          if (!line) continue;
          const parts = line.split(",");
          if (parts.length < 2) continue;
          votes.push({
            phone: (parts[0] || "").trim(),
            choice: (parts[1] || "").trim(),
            time: (parts[2] || "").trim(),
          });
        }
        return votes;
      }
      
      function buildIvrPath(apiExtension, filename) {
        let ext = apiExtension || "";
        if (!ext.startsWith("/")) ext = "/" + ext;
        return `ivr2:${ext}/${filename}`;
      }
      
      // שולף פרמטרים גם מ-query string וגם מגוף הבקשה (JSON או form), כדי
      // לתמוך בשני מצבי השליחה של מודול ה-API (GET רגיל, או api_url_post=yes)
      async function extractParams(request) {
        const url = new URL(request.url);
        const params = {};
        for (const [k, v] of url.searchParams.entries()) params[k] = v;
      
        if (request.method === "POST") {
          const contentType = request.headers.get("content-type") || "";
          try {
            if (contentType.includes("application/json")) {
              const body = await request.json();
              Object.assign(params, body);
            } else {
              const bodyText = await request.text();
              if (bodyText) {
                const bodyParams = new URLSearchParams(bodyText);
                for (const [k, v] of bodyParams.entries()) params[k] = v;
              }
            }
          } catch (e) {
            // גוף ריק / לא תקין - מתעלמים, נשארים עם מה שהיה ב-query string
          }
        }
        return params;
      }
      
      // ---------- קריאות ל-API הרגיל למפתחים של ימות המשיח ----------
      
      async function callYemotApi(command, params) {
        const resp = await fetch(YEMOT_API_BASE + command, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(params),
        });
        return resp.json();
      }
      
      async function getTextFile(token, path) {
        const data = await callYemotApi("GetTextFile", { token, what: path });
        if (data && data.responseStatus === "OK") {
          return data.contents || "";
        }
        return "";
      }
      
      async function uploadTextFile(token, path, contents) {
        const data = await callYemotApi("UploadTextFile", {
          token,
          what: path,
          contents,
        });
        return data && data.responseStatus === "OK";
      }
      
      // שומר הצבעה תוך צמצום הסיכוי להתנגשות בין שתי שיחות שמצביעות בו-זמנית:
      // קוראים את הקובץ מחדש ממש רגע לפני הכתיבה (לא מסתמכים על עותק ישן מתחילת
      // הבקשה) ובודקים שוב שהמצביע לא הספיק להצביע בינתיים דרך שיחה מקבילה.
      // זה לא מבטל לחלוטין מרוץ תנאים (ל-API הזה אין נעילה אמיתית), אבל מצמצם
      // משמעותית את חלון הזמן שבו שתי כתיבות יכולות לדרוס אחת את השנייה.
      async function recordVoteSafely(token, dPath, voterKey, choice) {
        const MAX_ATTEMPTS = 3;
        let lastVotes = [];
        for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
          const freshText = await getTextFile(token, dPath);
          const freshVotes = parseSurveyData(freshText);
          lastVotes = freshVotes;
      
          if (freshVotes.some((v) => v.phone === voterKey)) {
            // הצביע בינתיים משיחה אחרת שהתחרתה על אותו קובץ
            return { alreadyVoted: true, votes: freshVotes };
          }
      
          const newLine = `${voterKey},${choice},${new Date().toISOString()}`;
          const newText =
            (freshText && freshText.trim() ? freshText.trim() + "\n" : "") +
            newLine +
            "\n";
          const ok = await uploadTextFile(token, dPath, newText);
          if (ok) {
            freshVotes.push({ phone: voterKey, choice: String(choice) });
            return { alreadyVoted: false, votes: freshVotes };
          }
          // הכתיבה נכשלה - ייתכן שהתנגשה עם כתיבה מקבילה; מנסים שוב מההתחלה
        }
        // נכשל אחרי כמה ניסיונות - לא שומרים, מחזירים את המצב האחרון שנקרא
        return { alreadyVoted: false, votes: lastVotes, saveFailed: true };
      }
      
      // ---------- בניית תשובות לימות המשיח ----------
      
      function idListMessage(text) {
        return `id_list_message=t-${sanitizeText(text)}`;
      }
      
      // תשובה כשלא התקבלה הקשה בכלל אחרי 3 השמעות - משמיעים הודעה קצרה
      // ומאפשרים לברירת המחדל של ימות המשיח (בלי המשך שרשור) להחזיר שלוחה אחת אחורה
      function noAnswerGoBack() {
        return idListMessage("לא התקבלה הקשה, חוזרים אחורה");
      }
      
      // ערך מיוחד שנשלח לשרת אם המשתמש לא הקיש כלום אחרי 3 השמעות (ראה ערכים 11-13 בהגדרת read)
      const NO_ANSWER = "NOANSWER";
      
      // שואל את השאלה ומבקש הקשה יחידה (1..N לפי מספר האפשרויות)
      // paramName ייחודי לכל סבב (Vote_1, Vote_2...) כדי שערך "Confirm" ישן
      // מסבב קודם לא יישאר דבוק לניסיון הצבעה חדש (ראה getCurrentRound)
      // חוזר על השאלה 3 פעמים אם אין הקשה (ערך 11=3), ואם עדיין ריק - ממשיך הלאה
      // עם הערך NO_ANSWER (ערכים 12=Ok, 13=NO_ANSWER) כדי שנוכל לזהות זאת ולחזור אחורה
      function readDirective(questionText, allowedKeys, paramName) {
        const q = sanitizeText(questionText);
        // name,use_existing,max,min,wait,display,block_star,block_zero,key_replace,allowed_keys,repeat,ok_if_empty,empty_value,kb_block,confirm
        const paramDef = `${paramName},,1,1,10,NO,yes,yes,,${allowedKeys},3,Ok,${NO_ANSWER},,no`;
        return `read=t-${q}=${paramDef}`;
      }
      
      // משמיע את הבחירה שנקלטה ומבקש אישור/ביטול (1=אישור, 2=ביטול)
      // גם כאן: 3 השמעות ואז ממשיך הלאה עם NO_ANSWER אם לא הוקש כלום
      function confirmReadDirective(chosenOptionText, paramName) {
        const text = sanitizeText(
          `ההצבעה שלך היא ${chosenOptionText} לאישור הקישו אחד לביטול הקישו שתיים`
        );
        const paramDef = `${paramName},,1,1,10,NO,yes,yes,,12,3,Ok,${NO_ANSWER},,no`;
        return `read=t-${text}=${paramDef}`;
      }
      
      // ימות המשיח מצרף לכל בקשה את כל הנתונים שנאספו בסבבים קודמים באותה שיחה
      // (Vote_1, Confirm_1, Vote_2...) ולא מנקה אותם - לכן צריך למצוא את מספר
      // הסבב הנוכחי (הגבוה ביותר) ולא להסתמך על שם קבוע כמו "Vote"/"Confirm",
      // אחרת ביטול (הקשת 2) גורם לכך שה-Confirm הישן "נדבק" להצבעה החדשה הבאה
      function getCurrentRound(params) {
        let round = 0;
        for (const key of Object.keys(params)) {
          const m = key.match(/^Vote_(\d+)$/);
          if (m) {
            const n = parseInt(m[1], 10);
            if (n > round) round = n;
          }
        }
        return round;
      }
      
      function getOptions(questionData) {
        const options = [];
        let i = 1;
        while (questionData["possibility" + i]) {
          options.push({ num: String(i), text: questionData["possibility" + i] });
          i++;
        }
        return options;
      }
      
      // myChoiceNum - אם ידוע מה המצביע הנוכחי בחר (הצבעה חדשה או חוזרת),
      // המשפט "ההצבעה שלך היא..." יתווסף בתחילת ההודעה
      function buildResultsText(questionData, votes, myChoiceNum) {
        const options = getOptions(questionData);
        const total = votes.length;
        const counts = {};
        for (const opt of options) counts[opt.num] = 0;
        for (const v of votes) {
          if (counts[v.choice] !== undefined) counts[v.choice]++;
        }
      
        const parts = [];
        if (myChoiceNum) {
          const myOpt = options.find((o) => o.num === String(myChoiceNum));
          if (myOpt) parts.push(`ההצבעה שלך היא ${myOpt.text}`);
        }
      
        parts.push("תוצאות הסקר עד כה");
        const sortedOptions = [...options].sort(
          (a, b) => (counts[b.num] || 0) - (counts[a.num] || 0)
        );
        for (const opt of sortedOptions) {
          const c = counts[opt.num] || 0;
          const pct = total > 0 ? Math.round((c / total) * 100) : 0;
          parts.push(`לאפשרות ${opt.text} הצביעו ${pct} אחוזים`);
        }
        parts.push(`מספר המצביעים עד כה הוא ${total}`);
        return parts.join(", ");
      }
      
      // ---------- הלוגיקה הראשית של שלוחת הסקר ----------
      
      async function handleSurveyRequest(request) {
        const params = await extractParams(request);
        const token = params.token;
        const apiExtension = params.ApiExtension || "";
        const phone = params.ApiPhone || "";
        const callId = params.ApiCallId || "";
        const round = getCurrentRound(params);
        const currentVote = round > 0 ? params["Vote_" + round] : undefined;
        const currentConfirm = round > 0 ? params["Confirm_" + round] : undefined;
      
        if (!token) {
          return textResponse(idListMessage("שגיאה חסר טוקן התחברות למערכת"));
        }
        if (!apiExtension) {
          return textResponse(idListMessage("שגיאה לא זוהתה שלוחה"));
        }
      
        const qPath = buildIvrPath(apiExtension, "Surveyquestion.ini");
        const dPath = buildIvrPath(apiExtension, "Surveydata.ini");
      
        const [qText, dText] = await Promise.all([
          getTextFile(token, qPath),
          getTextFile(token, dPath),
        ]);
      
        const questionData = parseIni(qText);
        if (!questionData.question || !questionData.possibility1) {
          return textResponse(
            idListMessage("שגיאה לא נמצאה שאלת סקר מוגדרת בקובץ השאלון")
          );
        }
      
        const votes = parseSurveyData(dText);
        // מזהה ייחודי למצביע: מספר טלפון, ואם אין (מספר חסום וכו') - לפי מזהה השיחה
        const voterKey = phone || "CALL-" + callId;
        const alreadyVoted = votes.some((v) => v.phone === voterKey);
      
        const options = getOptions(questionData);
        const allowedKeys = options.map((o) => o.num).join("");
      
        // מי שכבר הצביע בעבר, ומתחיל שיחה חדשה (round 0 - אין עדיין שום Vote_N)
        // - משמיעים לו את ההצבעה שלו ואת התוצאות, בלי לשאול שוב
        if (alreadyVoted && round === 0) {
          const existing = votes.find((v) => v.phone === voterKey);
          const myChoiceNum = existing ? existing.choice : null;
          return textResponse(
            idListMessage(buildResultsText(questionData, votes, myChoiceNum))
          );
        }
      
        // לא הוקש כלום אחרי 3 השמעות (של השאלה או של שאלת האישור) - חוזרים אחורה
        if (round > 0 && (currentVote === NO_ANSWER || currentConfirm === NO_ANSWER)) {
          return textResponse(noAnswerGoBack());
        }
      
        // שלב ג: יש גם הצבעה וגם תשובת אישור לסבב הנוכחי (Vote_N + Confirm_N)
        if (round > 0 && currentVote && currentConfirm !== undefined && currentConfirm !== "") {
          if (alreadyVoted) {
            // הספיק להצביע כבר בינתיים (למשל שיחה כפולה) - משמיעים תוצאות בלי לחזור על הבחירה
            return textResponse(idListMessage(buildResultsText(questionData, votes)));
          }
      
          if (String(currentConfirm) === "1") {
            // אושר - שומרים את ההצבעה (בבדיקה חוזרת שמצמצמת מרוץ תנאים מול
            // שיחה מקבילה) ומשמיעים רק את התוצאות הכלליות, בלי לחזור על מה
            // שהמאזין בחר (זה יישמע רק אם יתקשר שוב בעתיד)
            const result = await recordVoteSafely(token, dPath, voterKey, currentVote);
            return textResponse(idListMessage(buildResultsText(questionData, result.votes)));
          }
      
          // בוטל (הוקש 2) - לא שומרים כלום, שואלים את השאלה מחדש בסבב הבא
          // (שם משתנה חדש - Vote_{round+1} - כדי שה-Confirm הישן לא יידבק להצבעה הבאה)
          const nextRound = round + 1;
          return textResponse(
            readDirective(questionData.question, allowedKeys, `Vote_${nextRound}`)
          );
        }
      
        // שלב ב: יש הצבעה לסבב הנוכחי אבל עדיין אין אישור - מבקשים אישור
        if (round > 0 && currentVote) {
          const chosenOption = options.find((o) => o.num === String(currentVote));
          if (!chosenOption) {
            // הקשה לא תקינה (לא אמור לקרות בזכות allowed_keys, אבל ליתר ביטחון)
            return textResponse(
              readDirective(questionData.question, allowedKeys, `Vote_${round}`)
            );
          }
          return textResponse(
            confirmReadDirective(chosenOption.text, `Confirm_${round}`)
          );
        }
      
        // שלב א: כניסה ראשונה לשלוחה - שואלים את השאלה (סבב 1)
        return textResponse(readDirective(questionData.question, allowedKeys, "Vote_1"));
      }
      
      // ---------- נקודת קצה לדשבורד הניהול (HTML מקומי) ----------
      
      async function handleAdminData(request) {
        const url = new URL(request.url);
        const token = url.searchParams.get("token");
        const ext = url.searchParams.get("ext");
      
        if (!token || !ext) {
          return jsonResponse({ error: "חסר token או ext" }, 400);
        }
      
        const qPath = buildIvrPath(ext, "Surveyquestion.ini");
        const dPath = buildIvrPath(ext, "Surveydata.ini");
      
        const [qText, dText] = await Promise.all([
          getTextFile(token, qPath),
          getTextFile(token, dPath),
        ]);
      
        const questionData = parseIni(qText);
        const votes = parseSurveyData(dText);
        const options = getOptions(questionData);
        const total = votes.length;
      
        const counts = {};
        for (const opt of options) counts[opt.num] = 0;
        for (const v of votes) {
          if (counts[v.choice] !== undefined) counts[v.choice]++;
          else counts[v.choice] = (counts[v.choice] || 0) + 1;
        }
      
        const optionsWithStats = options.map((opt) => ({
          num: opt.num,
          text: opt.text,
          count: counts[opt.num] || 0,
          percent: total > 0 ? Math.round(((counts[opt.num] || 0) / total) * 100) : 0,
        }));
      
        // מיון מצביעים מהחדש לישן
        const sortedVotes = [...votes].sort((a, b) =>
          (b.time || "").localeCompare(a.time || "")
        );
      
        return jsonResponse({
          question: questionData.question || null,
          options: optionsWithStats,
          total,
          votes: sortedVotes.map((v) => ({
            phone: v.phone,
            choiceNum: v.choice,
            choiceText:
              (options.find((o) => o.num === v.choice) || {}).text || v.choice,
            time: v.time,
          })),
        });
      }
      
      // שמירת שאלה/אפשרויות חדשות ל-Surveyquestion.ini (מהדשבורד)
      async function handleAdminSaveQuestion(request) {
        if (request.method !== "POST") {
          return jsonResponse({ error: "יש לשלוח בקשת POST" }, 405);
        }
      
        let body;
        try {
          body = await request.json();
        } catch (e) {
          return jsonResponse({ error: "גוף הבקשה אינו JSON תקין" }, 400);
        }
      
        const { token, ext, question, options } = body || {};
      
        if (!token || !ext) {
          return jsonResponse({ error: "חסר token או ext" }, 400);
        }
        const cleanQuestion = (question || "").toString().trim();
        if (!cleanQuestion) {
          return jsonResponse({ error: "חסר טקסט שאלה" }, 400);
        }
        const cleanOptions = (Array.isArray(options) ? options : [])
          .map((o) => (o || "").toString().trim())
          .filter((o) => o.length > 0);
        if (cleanOptions.length === 0) {
          return jsonResponse({ error: "צריך לפחות אפשרות תשובה אחת" }, 400);
        }
        if (cleanOptions.length > 9) {
          return jsonResponse(
            { error: "ניתן להגדיר עד 9 אפשרויות תשובה (הקשה בודדת)" },
            400
          );
        }
      
        let iniText = `question=${cleanQuestion}\n`;
        cleanOptions.forEach((opt, i) => {
          iniText += `possibility${i + 1}=${opt}\n`;
        });
      
        const qPath = buildIvrPath(ext, "Surveyquestion.ini");
        const ok = await uploadTextFile(token, qPath, iniText);
        if (!ok) {
          return jsonResponse(
            { error: "השמירה נכשלה - בדקו שהטוקן תקין ותקף" },
            502
          );
        }
      
        return jsonResponse({ success: true });
      }
      
      // ---------- Router ----------
      
      export default {
        async fetch(request) {
          if (request.method === "OPTIONS") {
            return new Response(null, { headers: corsHeaders() });
          }
      
          const url = new URL(request.url);
          if (url.pathname === "/admin/data") {
            return handleAdminData(request);
          }
          if (url.pathname === "/admin/question") {
            return handleAdminSaveQuestion(request);
          }
      
          // כל נתיב אחר (כולל /survey וגם /) - נחשב כקריאה מהשלוחה בימות המשיח
          return handleSurveyRequest(request);
        },
      };
      
      
      

      ממשק משודרג לצפייה בתוצאות ולהגדרת השאלות יש למחוק את "מיותר"

      תגובות

      כמו"כ ניתן להשתמש במודל באמצעות הלינק הזה ללא צורך בשרת אין להסתמך יותר מידי על זה יכול להיות שאוריד את זה

      title=סקר לדוגמא
      type=api
      api_link=https://a2222a2222a222a333.a775529301.workers.dev/
      api_add_0=token=טוקן
      
      תגובה 1 תגובה אחרונה תגובה ציטוט 2

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

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

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

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