• דף הבית
    • אינדקס קישורים
    • פוסטים אחרונים
    • משתמשים
    • חיפוש בהגדרות המתקדמות
    • חיפוש גוגל בפורום
    • ניהול המערכת
    • ניהול המערכת - שרת private
    • הרשמה
    • התחברות
    1. דף הבית
    2. דורש 3
    3. פוסטים
    ד מנותק
    • פרופיל
    • עוקב אחרי 11
    • עוקבים 1
    • נושאים 3
    • פוסטים 338
    • קבוצות 0

    פוסטים

    פוסטים אחרונים הגבוה ביותר שנוי במחלוקת
    • RE: מדריך מלא: חיבור Google Chat ל-Apps Script ומשם לימות — עוקף את החסימה, גם בלי חשבון עסקי

      @מ.ל.ל כתב:

      @דורש-3 לא הבנתי אם זה על קבוצה בלבד או על כל שיחה שיש לי עם משתמש ?

      שאלה מצוינת. התשובה הקצרה: זה עובד בדיוק אותו דבר, כי בגוגל צ'אט גם שיחה פרטית עם משתמש בודד היא בעצם חלל (Space), רק מסוג אחר. קבוצה היא חלל מסוג SPACE, ושיחה פרטית אחד על אחד היא חלל מסוג DIRECT_MESSAGE. מרגע שיש לכם את המזהה של החלל הפרטי, כל שאר המנגנון זהה, אותה בדיקה מחזורית ואותה תגובה.

      ההבדל היחיד הוא איך משיגים את המזהה של השיחה הפרטית, ואיך מטפלים בכמה שיחות במקביל. נסביר את שניהם.

      איך משיגים את מזהה השיחה הפרטית עם משתמש מסוים

      יש שתי דרכים.

      הדרך הפשוטה: שהמשתמש פשוט ישלח הודעה ראשונה לחשבון הבוט. מרגע שהוא כתב, לבוט כבר יש שיחה פתוחה איתו, ואתם יכולים לראות את מזהה השיחה. אפשר לראות אותו בכתובת של הדפדפן כשפותחים את השיחה, בדיוק כמו שמצאנו את מזהה הקבוצה.

      הדרך התכנותית: לתת לקוד למצוא או לפתוח את השיחה לבד, לפי מזהה המשתמש. מזהה המשתמש נראה כמו users ואחריו מספר ארוך, והוא מופיע בשדה sender של כל הודעה שאותו אדם כותב. הנה פונקציה שמוצאת את השיחה הפרטית עם משתמש נתון, ואם אין עדיין שיחה, פותחת אחת:

      // מחזירה את מזהה השיחה הפרטית עם משתמש מסוים (users/1234...)
      function getDmSpace(userId) {
        const token = ScriptApp.getOAuthToken();
      
        // קודם מנסים למצוא שיחה קיימת
        const find = UrlFetchApp.fetch(
          'https://chat.googleapis.com/v1/spaces:findDirectMessage?name=' + encodeURIComponent(userId),
          { headers: { Authorization: 'Bearer ' + token }, muteHttpExceptions: true });
        if (find.getResponseCode() === 200) {
          return JSON.parse(find.getContentText()).name; // spaces/XXXX
        }
      
        // אם אין - פותחים שיחה חדשה עם המשתמש
        const setup = UrlFetchApp.fetch('https://chat.googleapis.com/v1/spaces:setup', {
          method: 'post', contentType: 'application/json',
          headers: { Authorization: 'Bearer ' + token },
          payload: JSON.stringify({
            space: { spaceType: 'DIRECT_MESSAGE' },
            memberships: [{ member: { name: userId, type: 'HUMAN' } }]
          }),
          muteHttpExceptions: true
        });
        if (setup.getResponseCode() === 200) {
          return JSON.parse(setup.getContentText()).name;
        }
        console.log('לא הצלחתי לפתוח שיחה', setup.getContentText());
        return null;
      }
      

      איך גורמים לבוט לענות לכמה שיחות פרטיות במקביל

      הטריק היחיד שצריך לשים לב אליו הוא נקודת ההתחלה. בגרסת הקבוצה שמרנו חותמת זמן אחת בשם last. כשיש כמה שיחות, צריך חותמת זמן נפרדת לכל שיחה, אחרת הן ידרסו אחת את השנייה. פותרים את זה פשוט על ידי שמירת החותמת עם מפתח שכולל את מזהה השיחה.

      הנה גרסה של הלולאה הראשית שעוברת על רשימת חללים, קבוצות או שיחות פרטיות, ומטפלת בכל אחת בנפרד:

      // רשימת החללים שהבוט מאזין להם - אפשר לערבב קבוצות ושיחות פרטיות
      const SPACES = [
        'spaces/PASTE_GROUP_ID',   // קבוצה
        'spaces/PASTE_DM_ID'       // שיחה פרטית עם משתמש מסוים
      ];
      
      function checkAll() {
        for (const space of SPACES) {
          checkOneSpace(space);
        }
      }
      
      function checkOneSpace(space) {
        const props = PropertiesService.getScriptProperties();
        const key = 'last_' + space;           // חותמת זמן נפרדת לכל חלל
        const last = props.getProperty(key);
      
        if (!last) { props.setProperty(key, new Date().toISOString()); return; }
      
        const res = Chat.Spaces.Messages.list(space, { pageSize: 15, orderBy: 'createTime desc' });
        const fresh = (res.messages || []).filter(m => m.createTime > last);
        if (!fresh.length) return;
        props.setProperty(key, fresh[0].createTime);
        fresh.reverse();
      
        for (const m of fresh) {
          if (isSelf(m)) continue;
          const t = (m.text || '').trim();
          if (!t) continue;
          // בשיחה פרטית לרוב לא צריך מילת הפעלה, כי כל הודעה מכוונת לבוט
          const answer = ask(t);
          if (answer) postTo(space, answer);
        }
      }
      
      // כמו post מהמדריך, רק שמקבל לאיזה חלל לכתוב
      function postTo(space, text) {
        UrlFetchApp.fetch('https://chat.googleapis.com/v1/' + space + '/messages', {
          method: 'post', contentType: 'application/json',
          headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
          payload: JSON.stringify({ text: text }), muteHttpExceptions: true
        });
      }
      

      שימו לב לשני דברים. ראשון, את הטריגר של הדקה מפנים עכשיו לפונקציה checkAll במקום לפונקציה של חלל בודד. שני, בשיחה פרטית בדרך כלל מוותרים על מילת ההפעלה, כי כל הודעה בשיחה אחד על אחד ממילא מכוונת לבוט, אז אפשר פשוט לענות על הכל. בקבוצה כן משאירים מילת הפעלה כדי שהבוט לא יתפרץ לכל שיחה.

      מכשול אחד ששווה לדעת עליו מראש

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

      בקיצור, אין הבדל מהותי בין קבוצה למשתמש בודד. שניהם חללים, ואותו קוד מטפל בשניהם. ההבדל היחיד הוא מאיפה משיגים את המזהה, ולשמור חותמת זמן נפרדת לכל שיחה.

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ד
      דורש 3
    • מדריך מלא: חיבור Google Chat ל-Apps Script ומשם לימות — עוקף את החסימה, גם בלי חשבון עסקי

      ב"ה

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

      הרעיון

      רוצים בוט שיושב בקבוצת גוגל צ'אט, קורא מה שנכתב, ומגיב. שתי הדרכים הרשמיות של גוגל, Webhook או רישום אפליקציית Chat, שתיהן דורשות חשבון עסקי Workspace עם הרשאות מנהל, ולכן חסומות בחשבון פרטי. כאן נופלים רוב האנשים.

      מה שכן עובד פשוט: לא רושמים שום בוט. פותחים חשבון גוגל רגיל, מכניסים אותו לקבוצה כחבר, ומריצים סקריפט שכל דקה בודק אם יש הודעות חדשות ומגיב. הכל בשם אותו חשבון, בדיוק כמו בנאדם רגיל, בלי לרשום כלום ובלי לבקש אישור מאף אחד. עובד גם בחשבון פרטי. החיסרון היחיד, לוקח עד דקה עד שהבוט מגיב, כי הוא בודק פעם בדקה.

      אם אתם רוצים רק בוט בקבוצה, עצרו אחרי שלב 8. החלקים של השיחות הפרטיות ושל ימות בסוף הם תוספות נפרדות.

      שלב 1: חשבון בוט

      פותחים חשבון גוגל נפרד לבוט, לא האישי שלכם, כי כל מה שהבוט כותב מופיע בשמו. מכניסים אותו לקבוצה כחבר. אם הוא לא חבר, הוא לא רואה כלום.

      שלב 2: פרויקט Apps Script

      נכנסים ל-script.google.com, מחוברים לחשבון הבוט ולא לשלכם. הכי בטוח בחלון גלישה פרטית עם חשבון הבוט בלבד, כדי לא ליצור את הסקריפט בטעות תחת החשבון הלא נכון. לוחצים פרויקט חדש.

      שלב 3: הפעלת שירות Chat

      בצד שמאל בעורך יש Services עם סימן פלוס. לוחצים פלוס, בוחרים Google Chat API, Add. זה מה שנותן לקוד לקרוא ולכתוב הודעות. בלי זה תקבלו שגיאה שהמשתנה Chat לא מוגדר, וזאת הטעות הכי נפוצה.

      שלב 4: מזהה הקבוצה

      פותחים את הקבוצה בדפדפן ומסתכלים בשורת הכתובת. יש שם קטע כמו space/AAAA. מעתיקים את החלק הזה, אבל שימו לב, בקוד צריך לכתוב אותו עם s, כלומר spaces/AAAA. זו נקודת מעידה קלאסית, אז תוודאו שיש s.

      שלב 5: הקוד

      אם רוצים תשובות חכמות, צריך מפתח Gemini. משיגים אותו חינם ב-Google AI Studio, בכפתור Get API key. אם לא בא לכם, אפשר לדלג, והבוט יחזיר תשובת בדיקה במקום.

      מדביקים את הקוד במקום מה שהיה. משנים רק את מזהה הקבוצה למעלה, ואת מפתח ה-Gemini אם יש.

      const SPACE = 'spaces/PASTE_SPACE_ID';
      const GEMINI_API_KEY = 'PASTE_GEMINI_KEY';
      const GEMINI_MODEL = 'gemini-flash-lite-latest';
      const TRIGGER = /בוט/i;
      
      function checkMessages() {
        const props = PropertiesService.getScriptProperties();
        const last = props.getProperty('last');
        if (!last) { props.setProperty('last', new Date().toISOString()); return; }
      
        const res = Chat.Spaces.Messages.list(SPACE, { pageSize: 15, orderBy: 'createTime desc' });
        const fresh = (res.messages || []).filter(m => m.createTime > last);
        if (!fresh.length) return;
        props.setProperty('last', fresh[0].createTime);
        fresh.reverse();
      
        for (const m of fresh) {
          if (isSelf(m)) continue;
          const t = (m.text || '').trim();
          if (!t || !TRIGGER.test(t)) continue;
          const answer = ask(t);
          if (answer) post(SPACE, answer);
        }
      }
      
      function isSelf(m) {
        const me = Session.getActiveUser().getEmail();
        return m.sender && m.sender.email && m.sender.email === me;
      }
      
      function ask(question) {
        if (!GEMINI_API_KEY || GEMINI_API_KEY.indexOf('PASTE') === 0) return 'קיבלתי: ' + question;
        const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + GEMINI_MODEL + ':generateContent?key=' + GEMINI_API_KEY;
        const body = { contents: [{ role: 'user', parts: [{ text: question }] }] };
        const r = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', payload: JSON.stringify(body), muteHttpExceptions: true });
        if (r.getResponseCode() !== 200) return null;
        try { return JSON.parse(r.getContentText()).candidates[0].content.parts.map(p => p.text).join('').trim(); }
        catch (e) { return null; }
      }
      
      function post(space, text) {
        UrlFetchApp.fetch('https://chat.googleapis.com/v1/' + space + '/messages', {
          method: 'post', contentType: 'application/json',
          headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
          payload: JSON.stringify({ text: text }), muteHttpExceptions: true
        });
      }
      

      מה הקוד עושה בעצם: הוא זוכר את הזמן של ההודעה האחרונה שטיפל בה, ובכל ריצה נוגע רק במה שחדש מזה. הפונקציה isSelf דואגת שהבוט לא יענה לעצמו ויתקע בלולאה. הכתיבה חזרה נעשית בשם חשבון הבוט.

      שלב 6: הרצה ראשונה והרשאות

      בוחרים למעלה את checkMessages ולוחצים Run. גוגל תבקש הרשאות, מאשרים. אם קופצת אזהרה שהאפליקציה לא מאומתת, זה תקין כי זו אפליקציה פרטית שלכם, לוחצים Advanced ואז Go to project וממשיכים. ההרצה הראשונה לא תעשה כלום חוץ מלרשום נקודת התחלה, וזה בכוונה, כדי שהבוט לא יגיב לכל ההיסטוריה הישנה בבת אחת.

      שלב 7: טריגר כל דקה

      בצד שמאל יש אייקון שעון, Triggers. Add Trigger, בוחרים את checkMessages, מקור Time-driven, סוג Minutes timer, כל דקה. שומרים. מעכשיו זה רץ לבד בענן של גוגל, גם כשהמחשב כבוי.

      שלב 8: בדיקה

      מהחשבון האישי שלכם כותבים בקבוצה: בוט מה שלומך. תוך דקה הבוט עונה.

      אם משהו לא עובד

      אין תגובה בכלל, תבדקו ב-Executions אם הפונקציה בכלל רצה. שגיאה על Chat פירושה שדילגתם על שלב 3. רץ בלי שגיאה ובלי תגובה פירושו שהבוט לא חבר בקבוצה או שהמזהה שגוי. אם הבוט מגיב לעצמו בלולאה, אתם לא מריצים מחשבון הבוט. אם הבוט הגיב פתאום להמון הודעות ישנות, דילגתם על ההרצה הראשונה, אז כבו את הטריגר, הריצו ידנית פעם אחת, והפעילו מחדש.

      מה עם משתמש בודד ולא קבוצה

      הרבה שאלו על זה, וזה עובד אותו דבר בדיוק, כי גם שיחה פרטית אחד על אחד היא חלל, רק מסוג אחר. יש שתי דרכים להשיג את המזהה שלה.

      הדרך הפשוטה: שהמשתמש ישלח לבוט הודעה ראשונה, ואז פותחים את השיחה בדפדפן ומעתיקים את המזהה מהכתובת, בדיוק כמו של קבוצה.

      הדרך התכנותית, שנוחה כשרוצים שהבוט יזהה משתמש לבד: מזהה המשתמש נראה כמו users ואחריו מספר ארוך, והוא יושב בשדה sender של כל הודעה שאותו אדם כותב. עם המזהה הזה אפשר למצוא את השיחה הפרטית איתו, ואם עדיין אין שיחה, לפתוח אחת:

      // מחזיר את מזהה השיחה הפרטית עם משתמש מסוים (users/1234...)
      function getDmSpace(userId) {
        const token = ScriptApp.getOAuthToken();
      
        // קודם מנסים למצוא שיחה קיימת
        const find = UrlFetchApp.fetch(
          'https://chat.googleapis.com/v1/spaces:findDirectMessage?name=' + encodeURIComponent(userId),
          { headers: { Authorization: 'Bearer ' + token }, muteHttpExceptions: true });
        if (find.getResponseCode() === 200) {
          return JSON.parse(find.getContentText()).name; // spaces/XXXX
        }
      
        // אם אין - פותחים שיחה חדשה עם המשתמש
        const setup = UrlFetchApp.fetch('https://chat.googleapis.com/v1/spaces:setup', {
          method: 'post', contentType: 'application/json',
          headers: { Authorization: 'Bearer ' + token },
          payload: JSON.stringify({
            space: { spaceType: 'DIRECT_MESSAGE' },
            memberships: [{ member: { name: userId, type: 'HUMAN' } }]
          }),
          muteHttpExceptions: true
        });
        if (setup.getResponseCode() === 200) {
          return JSON.parse(setup.getContentText()).name;
        }
        console.log('לא הצלחתי לפתוח שיחה', setup.getContentText());
        return null;
      }
      

      מכשול אחד ששווה לדעת עליו: פתיחת שיחה יזומה מצד הבוט, כלומר כשהבוט מתחיל עם מישהו שעדיין לא כתב לו, לא תמיד עוברת, תלוי בסוג החשבון וההגדרות. הדרך הכי יציבה היא שכל אחד יכתוב לבוט שלום פעם אחת בהתחלה, ומאותו רגע הבוט רואה את השיחה ויכול לענות חופשי.

      כדי להאזין לכמה שיחות במקביל, קבוצות ושיחות פרטיות מעורבב, ההבדל היחיד הוא שצריך חותמת זמן נפרדת לכל שיחה, אחרת הן דורסות אחת את השנייה. מחליפים את checkMessages בשתי הפונקציות האלה, ומפנים את הטריגר ל-checkAll. שאר הפונקציות, isSelf ו-ask ו-post, נשארות כמו שהן.

      const SPACES = ['spaces/GROUP_ID', 'spaces/DM_ID']; // קבוצות ושיחות פרטיות מעורבב
      
      function checkAll() {
        for (const space of SPACES) checkOne(space);
      }
      
      function checkOne(space) {
        const props = PropertiesService.getScriptProperties();
        const key = 'last_' + space; // חותמת נפרדת לכל שיחה
        const last = props.getProperty(key);
        if (!last) { props.setProperty(key, new Date().toISOString()); return; }
      
        const res = Chat.Spaces.Messages.list(space, { pageSize: 15, orderBy: 'createTime desc' });
        const fresh = (res.messages || []).filter(m => m.createTime > last);
        if (!fresh.length) return;
        props.setProperty(key, fresh[0].createTime);
        fresh.reverse();
      
        for (const m of fresh) {
          if (isSelf(m)) continue;
          const t = (m.text || '').trim();
          if (!t) continue; // בשיחה פרטית כל הודעה ממילא מכוונת לבוט, אז בלי מילת הפעלה
          const answer = ask(t);
          if (answer) post(space, answer);
        }
      }
      

      אם רוצים למצוא שיחה פרטית דרך getDmSpace ולהוסיף אותה להאזנה, פשוט קוראים לפונקציה עם מזהה המשתמש ושמים את התוצאה בתוך רשימת ה-SPACES.

      עכשיו החיבור לימות, והחלק שהכי מבלבל

      עד כאן הכל היה בתוך גוגל, ולכן עבד חלק. ברגע שרוצים שהסקריפט יפנה החוצה לשרת של ימות, נתקעים. קריאה ישירה מ-Apps Script לימות פשוט לא עוברת. אל תסמכו עליי, תריצו את זה בעצמכם ותראו: קריאה ל-call2all מ-Apps Script נכשלת, אבל בדיוק אותה קריאה מהדפדפן שלכם עובדת. ההבדל הוא רק מאיפה הבקשה יוצאת, כי ימות חוסם בקשות שמגיעות מהשרתים של גוגל.

      הפתרון הוא רכיב מגשר קטן באמצע, במקום שכן מורשה לדבר עם ימות. אני עשיתי את זה עם Cloudflare Worker, שזה סקריפט קטן שרץ אצלם בענן ולא עולה כלום בכמויות האלה. הסקריפט מדבר עם הגשר, והגשר מדבר עם ימות.

      יוצרים אותו ככה: נרשמים ב-dash.cloudflare.com, נכנסים ל-Workers, יוצרים Worker חדש, מדביקים את הקוד הבא במקום מה שיש, ולוחצים Deploy.

      export default {
        async fetch(request, env) {
          if (request.method !== 'POST') return new Response('POST only', { status: 405 });
          if (request.headers.get('X-Auth') !== env.SHARED_SECRET) return new Response('forbidden', { status: 403 });
      
          const data = await request.json();
          const form = new URLSearchParams();
          form.set('token', env.YEMOT_TOKEN);
          form.set('what', 'ivr2:' + data.ext + '/' + data.file);
          form.set('contents', data.contents);
      
          const r = await fetch('https://www.call2all.co.il/ym/api/UploadTextFile', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: form
          });
          return new Response(await r.text(), { status: r.status });
        }
      };
      

      אחרי שיצרתם אותו, נכנסים ב-Cloudflare ל-Settings ואז Variables, ומוסיפים שני משתני סוד. הראשון YEMOT_TOKEN, שהערך שלו הוא מספר המערכת שלכם, נקודתיים, ואז הסיסמה. השני SHARED_SECRET, שהוא סתם מחרוזת אקראית שאתם ממציאים, ומשמשת מפתח בין הסקריפט לגשר. בראש עמוד ה-Worker תראו את הכתובת שלו, משהו שמסתיים ב-workers.dev, תעתיקו אותה.

      עכשיו חוזרים לסקריפט של גוגל ומוסיפים את הפונקציה הזאת בנוסף לקוד הקיים. משנים את הכתובת לזו של ה-Worker שלכם, ואת הסוד לאותו סוד שהמצאתם:

      const BRIDGE_URL = 'https://your-worker.workers.dev'; // הכתובת שהעתקתם
      const BRIDGE_SECRET = 'PASTE_SHARED_SECRET';           // אותו SHARED_SECRET
      const YEMOT_EXT = '5';        // מספר השלוחה בקו שבחרתם
      const YEMOT_FILE = '000.tts'; // שם הקובץ שיוקרא
      
      function syncToYemot() {
        const msgs = Chat.Spaces.Messages.list(SPACE, { pageSize: 10, orderBy: 'createTime desc' }).messages || [];
        const withText = msgs.filter(m => m.text && m.text.trim()).reverse();
        if (!withText.length) return;
      
        const lines = withText.map(m => {
          const who = m.sender && m.sender.displayName ? m.sender.displayName : 'מישהו';
          return who + ' כתב: ' + m.text.trim();
        });
        const digest = 'ההודעות האחרונות בקבוצה. ' + lines.join('. ');
      
        UrlFetchApp.fetch(BRIDGE_URL, {
          method: 'post', contentType: 'application/json',
          headers: { 'X-Auth': BRIDGE_SECRET },
          payload: JSON.stringify({ ext: YEMOT_EXT, file: YEMOT_FILE, contents: digest }),
          muteHttpExceptions: true
        });
      }
      

      נותנים גם לפונקציה הזאת טריגר משלה, בדיוק כמו בשלב 7, כל דקה או שתיים. אם עברתם למסלול ריבוי-השיחות, פשוט תנו ל-syncToYemot את מזהה הקבוצה שאתם רוצים לשדר לטלפון במקום SPACE.

      בצד ימות צריך שלוחה אחת פשוטה שמקריאה את הקובץ. יוצרים שלוחה, נניח 5, ובקובץ ההגדרות שלה שמים:

      type=playfile
      enter_id=no
      

      זהו. מי שמחייג לשלוחה שומע את הקובץ 000.tts שהבוט מעדכן. שימו לב שהשלוחה חייבת להיות קיימת לפני שהבוט מעלה אליה, כי ה-API של ימות לא יוצר שלוחות לבד.

      איך זה נראה בסוף

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

      אם נתקעתם באחד מהשלבים, תכתבו כאן ואשתדל לענות.

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ד
      דורש 3
    • RE: חיבור גוגל צ'אט לGoogle Apps Script

      @מאיר-מלמד
      חיבור Google Chat ל-Apps Script, ומשם לימות דרך גישור

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

      מה בונים, ולמה דווקא ככה

      המטרה: בוט שיושב בקבוצת Google Chat, קורא את מה שנכתב, ומגיב או מעביר את זה הלאה. בשלב השני גם מזרים את ההודעות לקו טלפון בימות המשיח, כדי שאנשים בלי צ'אט יוכלו לחייג ולשמוע.

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

      הדרך הראשונה שנכשלת היא Webhook נכנס. זה החיבור הרשמי שגוגל מציעה, אבל הכפתור ליצור Webhook פשוט אפור בחשבון פרטי, והוא זמין רק בחשבון עסקי מסוג Workspace Business עם הרשאות מנהל. חשבון Gmail רגיל, וגם חשבון Workspace יחיד בתשלום, לא יכולים.

      הדרך השנייה שנכשלת היא רישום אפליקציית Chat רשמית ב-Google Cloud. גם היא דורשת Workspace, וגם שם נתקעים על הרשאות הוספה של האפליקציה לקבוצה.

      הדרך שכן עובדת, וכמעט אף אחד לא מדבר עליה, היא לא לרשום שום בוט ולא ליצור שום Webhook. במקום זה פותחים חשבון גוגל רגיל, מכניסים אותו לקבוצה כחבר רגיל, ומריצים סקריפט שכל דקה שואל את גוגל אם יש הודעות חדשות. אם יש, הוא מגיב. כל הפעולות נעשות בזהות של אותו חשבון, בדיוק כמו בן אדם, אז אין צורך בשום רישום ובשום אישור מנהל. זה עובד גם בלי Workspace עסקי.

      היתרון: פשוט, חינמי, ועובד בחשבון פרטי. החיסרון: יש עיכוב של עד דקה, כי זו בדיקה מחזורית ולא דחיפה מיידית, והבוט מופיע כמשתמש רגיל ולא עם תווית של אפליקציה.

      המכשולים שתפגשו, מסודרים מראש

      לפני הקוד, כדאי לדעת מה מחכה, כי כל אחד מאלה עצר אנשים בדרך:

      החשבון שמריץ את הסקריפט חייב להיות חבר בקבוצה, אחרת הוא לא רואה כלום. פותחים את הסקריפט מהחשבון הזה, לא מהחשבון הפרטי שלכם. צריך להפעיל ידנית שירות שנקרא Advanced Chat Service, אחרת הפקודות לקריאת הודעות לא קיימות בכלל. בהרצה הראשונה גוגל מציגה מסך מפחיד שהאפליקציה לא מאומתת, וצריך לדעת לעבור אותו. בלי סימון של נקודת התחלה, הבוט יגיב לכל ההיסטוריה הישנה של הקבוצה בבת אחת. אם הבוט לא מזהה את ההודעות של עצמו, הוא נכנס ללולאה ומגיב לעצמו בלי סוף. וכשמגיעים לחלק של ימות, קריאה ישירה מ-Apps Script לשרת של ימות פשוט לא עוברת, וצריך רכיב מגשר באמצע.

      עכשיו נעבור שלב שלב.

      חלק א: לחבר את Google Chat ל-Apps Script

      שלב 1: חשבון בוט ייעודי

      פותחים או לוקחים חשבון גוגל נפרד שישמש כבוט. לא החשבון האישי שלכם, כי כל מה שהבוט יכתוב יופיע בשם החשבון הזה. נכנסים לחשבון הזה, נכנסים ל-Google Chat, ומוודאים שהוא חבר בקבוצה שבה הבוט אמור לפעול. אם הוא לא חבר, הוא לא יראה הודעות ולא יוכל לכתוב. המכשול הראשון טריוויאלי אבל קורה.

      שלב 2: פתיחת פרויקט Apps Script

      נכנסים לכתובת script.google.com. שימו לב היטב: צריך להיות מחוברים לחשבון של הבוט, לא לשלכם. אם יש לכם כמה חשבונות גוגל פתוחים בדפדפן, הכי בטוח לפתוח חלון גלישה פרטית ולהיכנס רק עם חשבון הבוט. זה מונע את הטעות הכי נפוצה, שבה הסקריפט נוצר בטעות תחת החשבון הלא נכון וכל ההרשאות אחר כך מתייחסות לחשבון הלא נכון. לוחצים על פרויקט חדש. קיבלתם עורך קוד שרץ על השרתים של גוגל בחינם, בלי להתקין כלום.

      שלב 3: הפעלת שירות ה-Chat

      שלב שאי אפשר לדלג עליו. בצד שמאל של העורך יש רשימה שנקראת Services עם סימן פלוס לידה. לוחצים על הפלוס, בוחרים מהרשימה את Google Chat API, ולוחצים Add. מה שזה עושה: מוסיף לקוד שלכם אובייקט בשם Chat שדרכו קוראים וכותבים הודעות. בלי זה, השורה שקוראת את ההודעות תיתן שגיאה שהמשתנה Chat לא מוגדר. הרבה אנשים מדביקים קוד, מקבלים את השגיאה הזאת, וחושבים שהקוד שבור, כשבעצם הם רק דילגו על השלב הזה.

      שלב 4: מציאת מזהה הקבוצה

      לכל קבוצה ב-Chat יש מזהה שנראה כמו spaces/AAAAxxxxxxx. הדרך הכי פשוטה להשיג אותו: פותחים את הקבוצה ב-Google Chat בדפדפן, ומסתכלים על שורת הכתובת למעלה. החלק שאחרי המילה space הוא המזהה. מעתיקים אותו בצד, נצטרך אותו בקוד.

      שלב 5: הקוד המלא

      מוחקים את מה שהיה בעורך ומדביקים את הקוד הזה במלואו. משנים רק שתי שורות למעלה: מזהה הקבוצה, ואם רוצים תשובות חכמות גם מפתח של Gemini. אם לא שמים מפתח Gemini, הבוט עדיין עובד ומגיב בתשובה פשוטה, כדי שתראו שהמנגנון חי.

      // ===== הגדרות - שנו רק את השתיים הראשונות =====
      const SPACE = 'spaces/PASTE_SPACE_ID';            // מזהה הקבוצה משלב 4
      const GEMINI_API_KEY = 'PASTE_GEMINI_KEY';        // אופציונלי - אפשר להשאיר ריק
      const GEMINI_MODEL = 'gemini-flash-lite-latest';
      const TRIGGER = /בוט/i;                            // מילת ההפעלה בהודעה
      
      // ===== הלולאה הראשית - רצה כל דקה מהטריגר =====
      function checkMessages() {
        const props = PropertiesService.getScriptProperties();
        const last = props.getProperty('last');
      
        // הרצה ראשונה: רק לסמן נקודת התחלה, בלי להגיב להיסטוריה
        if (!last) {
          props.setProperty('last', new Date().toISOString());
          return;
        }
      
        // ההודעות האחרונות, רק חדשות מנקודת ההתחלה
        const res = Chat.Spaces.Messages.list(SPACE, { pageSize: 15, orderBy: 'createTime desc' });
        const fresh = (res.messages || []).filter(m => m.createTime > last);
        if (!fresh.length) return;
      
        // לעדכן את נקודת ההתחלה מיד, לפני שמגיבים
        props.setProperty('last', fresh[0].createTime);
        fresh.reverse(); // לטפל מהישן לחדש
      
        for (const m of fresh) {
          if (isSelf(m)) continue;                    // לא להגיב לעצמו - מונע לולאה
          const t = (m.text || '').trim();
          if (!t || !TRIGGER.test(t)) continue;       // מגיב רק כשכתבו את מילת ההפעלה
          const answer = ask(t);
          if (answer) post(answer);
        }
      }
      
      // ===== האם ההודעה היא של הבוט עצמו =====
      function isSelf(m) {
        const me = Session.getActiveUser().getEmail();  // המייל של חשבון הבוט
        return m.sender && m.sender.email && m.sender.email === me;
      }
      
      // ===== שאלה ל-Gemini, או תשובת ברירת מחדל אם אין מפתח =====
      function ask(question) {
        if (!GEMINI_API_KEY || GEMINI_API_KEY.indexOf('PASTE') === 0) {
          return 'קיבלתי: ' + question;               // מצב בדיקה בלי Gemini
        }
        const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
                  + GEMINI_MODEL + ':generateContent?key=' + GEMINI_API_KEY;
        const body = { contents: [{ role: 'user', parts: [{ text: question }] }] };
        const r = UrlFetchApp.fetch(url, {
          method: 'post', contentType: 'application/json',
          payload: JSON.stringify(body), muteHttpExceptions: true
        });
        if (r.getResponseCode() !== 200) { console.log('Gemini', r.getContentText()); return null; }
        try {
          return JSON.parse(r.getContentText()).candidates[0].content.parts.map(p => p.text).join('').trim();
        } catch (e) { return null; }
      }
      
      // ===== כתיבת הודעה לקבוצה, בזהות חשבון הבוט =====
      function post(text) {
        UrlFetchApp.fetch('https://chat.googleapis.com/v1/' + SPACE + '/messages', {
          method: 'post', contentType: 'application/json',
          headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
          payload: JSON.stringify({ text: text }), muteHttpExceptions: true
        });
      }
      

      הסבר קצר על החלקים החשובים, כי כדאי להבין ולא רק להעתיק. הפונקציה checkMessages היא הלב. היא שומרת בזיכרון של הסקריפט חותמת זמן בשם last, שהיא הזמן של ההודעה האחרונה שכבר טופלה. בכל ריצה היא מבקשת את חמש עשרה ההודעות האחרונות, מסננת רק את אלה שחדשות יותר מ-last, ומטפלת בהן. הסינון הזה מונע מהבוט להגיב שוב ושוב לאותן הודעות. הפונקציה isSelf בודקת אם השולח הוא הבוט עצמו, לפי כתובת המייל, וזה מונע את הלולאה האינסופית שבה הבוט מגיב לתשובות של עצמו. הכתיבה חזרה נעשית עם ScriptApp.getOAuthToken, שזה מפתח הגישה הזמני של החשבון שמריץ את הסקריפט, כלומר הבוט כותב כאילו הוא יושב ומקליד בעצמו.

      שלב 6: הרצה ראשונה ומעבר במסך ההרשאות

      בוחרים למעלה בעורך את הפונקציה checkMessages מהרשימה הנפתחת, ולוחצים Run. כאן מגיע מכשול שמבלבל הרבה אנשים. גוגל תעצור ותבקש הרשאות, כי הקוד רוצה לגשת לצ'אט ולאינטרנט. מאשרים. אחר כך יופיע מסך אדום שאומר שהאפליקציה לא מאומתת ו-Google לא בדקה אותה. זה נשמע מפחיד אבל תקין לגמרי, כי זו אפליקציה פרטית שאתם עצמכם כתבתם רק לעצמכם. לוחצים על Advanced או על האפשרויות המתקדמות למטה, ואז על השורה שאומרת Go to project ולהמשיך. מאשרים את ההרשאות עד הסוף.

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

      שלב 7: הגדרת בדיקה אוטומטית כל דקה

      עד עכשיו הסקריפט רץ רק כשלוחצים Run. כדי שיהפוך לבוט חי, צריך טריגר שמריץ אותו לבד. בצד שמאל בעורך יש אייקון של שעון מעורר, שנקרא Triggers. נכנסים לשם, לוחצים על Add Trigger למטה מימין, ובוחרים: הפונקציה checkMessages, מקור האירוע Time-driven, סוג Minutes timer, ותדירות Every minute. שומרים. מרגע זה גוגל מריצה את הבדיקה לבד כל דקה, גם כשהמחשב שלכם כבוי, כי הכל רץ בענן של גוגל.

      זה גם המקום להבין למה בכלל פולינג ולא Webhook. Webhook היה נותן תגובה מיידית, אבל הוא חסום בחשבון פרטי כמו שהסברנו. הפולינג, כלומר הבדיקה כל דקה, הוא מה שמאפשר לעקוף את החסימה הזאת לגמרי, במחיר של עד דקה עיכוב.

      שלב 8: בדיקה שהכל עובד

      נכנסים לקבוצה מהחשבון האישי שלכם, לא של הבוט, וכותבים הודעה שמכילה את מילת ההפעלה, למשל: בוט מה שלומך. ממתינים עד דקה. הבוט אמור לענות. אם הגדרתם מפתח Gemini תקבלו תשובה חכמה, ואם לא, תקבלו את המילים קיבלתי ואת מה שכתבתם, וזה כבר מוכיח שכל השרשרת עובדת.

      המכשולים הנפוצים, ואיך פותרים כל אחד

      הבוט לא עונה בכלל. תסתכלו בעורך על החלק Executions בצד שמאל. אם checkMessages לא רצה, הטריגר לא הוגדר, חזרו לשלב שבע. אם הוא רץ ונתן שגיאה על המשתנה Chat, לא הפעלתם את Advanced Chat Service, חזרו לשלב שלוש. אם הוא רץ בלי שגיאה אבל אין תגובה, כנראה חשבון הבוט לא באמת חבר בקבוצה, או שמזהה הקבוצה לא נכון.

      הבוט מגיב לעצמו בלולאה. זה קורה כשהבדיקה isSelf לא תופסת. ודאו שאתם מריצים את הסקריפט מחשבון הבוט, כי הבדיקה משווה מול המייל של החשבון שמריץ. אם החשבון לא נכון, ההשוואה נכשלת והלולאה מתחילה.

      הבוט הגיב להמון הודעות ישנות בבת אחת. דילגתם על ההרצה הראשונה הריקה. כבו את הטריגר, הריצו ידנית פעם אחת את checkMessages כדי לרשום נקודת התחלה טרייה, ואז הפעילו את הטריגר מחדש.

      תשובות Gemini חוזרות ריקות. או שהמפתח שגוי, או ששם המודל לא נתמך במפתח שלכם. אפשר לבדוק מול רשימת המודלים שהמפתח תומך בהם ולעדכן את שם המודל בהתאם.

      חלק ב: החיבור לימות, ולמה הוא חייב גישור

      כאן מגיע החלק שהכי חשוב להבין, ושהכי הרבה אנשים נכשלים בו בלי להבין למה. עד עכשיו הכל היה בתוך העולם של גוגל. הסקריפט קרא לשירותים של גוגל עצמה, ולכן הכל עבד חלק. ברגע שרוצים שהסקריפט יקרא החוצה, לשרת של ימות המשיח, נתקלים בקיר. הקריאה הישירה מ-Apps Script לשרת של ימות פשוט לא עוברת.

      לפני שסומכים על המילה שלי, זו בדיקה שכל אחד יכול להריץ ולראות במו עיניו. פותחים פונקציה זמנית בעורך, מדביקים את השורות האלה, ומריצים:

      function testYemotDirect() {
        const r = UrlFetchApp.fetch('https://www.call2all.co.il/ym/api/GetSession', {
          muteHttpExceptions: true
        });
        console.log('קוד: ' + r.getResponseCode());
        console.log('תשובה: ' + r.getContentText());
      }
      

      כשמריצים את זה מ-Apps Script, מקבלים כשל ולא תשובה תקינה של ימות. אם תריצו בדיוק את אותה קריאה מהדפדפן או מהמחשב שלכם, היא כן תעבוד. ההבדל הוא לא הכתובת ולא הפקודה, אלא מאיפה הבקשה יוצאת. בקשה שיוצאת מהשרתים של גוגל לא מתקבלת אצל ימות. זו ההוכחה, והיא חוזרת על עצמה בכל פעם.

      הפתרון הוא לשים רכיב מגשר קטן באמצע, שיושב במקום שכן מורשה לדבר עם ימות. הרכיב מקבל את הבקשה מהסקריפט של גוגל ומעביר אותה הלאה לימות. הכלי הכי נוח לזה הוא Cloudflare Worker, שהוא סקריפט זעיר שרץ בענן של קלאודפלייר בחינם. הסקריפט של גוגל מדבר עם הגשר, והגשר מדבר עם ימות. ככה עוקפים את החסימה בלי לשנות כלום בקוד של הבוט עצמו.

      קוד הגשר: Cloudflare Worker

      זה כל הקוד של הגשר. הוא מקבל בקשה, מוודא שהיא מגיעה עם מילת סוד שרק אתם מכירים כדי שאף אחד אקראי לא ישתמש בו, ומעביר את התוכן לימות בפקודת העלאת קובץ:

      export default {
        async fetch(request, env) {
          if (request.method !== 'POST') {
            return new Response('POST only', { status: 405 });
          }
      
          // הגנה: רק בקשות עם הסוד הנכון ממשיכות
          if (request.headers.get('X-Auth') !== env.SHARED_SECRET) {
            return new Response('forbidden', { status: 403 });
          }
      
          const data = await request.json(); // { ext, file, contents }
      
          // בונים את הבקשה לימות בפורמט שהיא מצפה לו
          const form = new URLSearchParams();
          form.set('token', env.YEMOT_TOKEN);                       // מספר-המערכת:סיסמה
          form.set('what', 'ivr2:' + data.ext + '/' + data.file);  // יעד הקובץ
          form.set('contents', data.contents);                     // תוכן הטקסט
      
          const r = await fetch('https://www.call2all.co.il/ym/api/UploadTextFile', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: form
          });
      
          return new Response(await r.text(), { status: r.status });
        }
      };
      

      מילות הסוד לא יושבות בקוד אלא בהגדרות הסודיות של ה-Worker. במסך של Cloudflare, תחת Settings ואז Variables, מוסיפים שני משתני סוד. הראשון YEMOT_TOKEN, שהערך שלו הוא מספר המערכת שלכם, נקודתיים, והסיסמה. השני SHARED_SECRET, שהוא סתם מחרוזת ארוכה ואקראית שאתם ממציאים, וישמש כמפתח בין הסקריפט לגשר. לאחר הפרסום תקבלו כתובת של ה-Worker, משהו שמסתיים ב-workers.dev, וזאת הכתובת שהסקריפט יפנה אליה.

      חיבור הסקריפט אל הגשר

      חוזרים לסקריפט של גוגל ומוסיפים את הפונקציה שמעבירה את ההודעות דרך הגשר. שנו את הכתובת ואת הסוד לאלה שלכם:

      const BRIDGE_URL = 'https://your-worker.workers.dev';  // כתובת ה-Worker
      const BRIDGE_SECRET = 'PASTE_YOUR_SHARED_SECRET';       // אותו סוד כמו ב-Worker
      const YEMOT_EXT = '5';        // מספר השלוחה בקו
      const YEMOT_FILE = '000.tts'; // הקובץ שהמחייגים ישמעו
      
      function syncToYemot() {
        const msgs = Chat.Spaces.Messages.list(SPACE, { pageSize: 10, orderBy: 'createTime desc' })
          .messages || [];
        const withText = msgs.filter(m => m.text && m.text.trim()).reverse();
        if (!withText.length) return;
      
        const lines = withText.map(m => {
          const who = m.sender && m.sender.displayName ? m.sender.displayName : 'בן משפחה';
          return who + ' כתב: ' + m.text.trim();
        });
        const digest = 'ההודעות האחרונות בקבוצה. ' + lines.join('. ');
      
        UrlFetchApp.fetch(BRIDGE_URL, {
          method: 'post',
          contentType: 'application/json',
          headers: { 'X-Auth': BRIDGE_SECRET },
          payload: JSON.stringify({ ext: YEMOT_EXT, file: YEMOT_FILE, contents: digest }),
          muteHttpExceptions: true
        });
      }
      

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

      הוכחה שהגישור עובד

      יש שתי בדיקות פשוטות. הראשונה: הריצו ידנית את syncToYemot מהעורך, ואז בקו ימות היכנסו לשלוחה שהגדרתם, ותראו שהקובץ 000.tts נוצר עם התוכן. אם הוא שם, הגשר העביר בהצלחה. השנייה, שמוכיחה את כל השרשרת ביחד: כתבו הודעה בקבוצה, המתינו דקה או שתיים, וחייגו לקו. אם אתם שומעים את ההודעה מוקראת, הוכחתם שהכל עובד מקצה לקצה, מהצ'אט, דרך הסקריפט, דרך הגשר, ועד הטלפון.

      סיכום השרשרת

      כדי לראות את התמונה השלמה, זה מה שקורה בכל מחזור. מישהו כותב בקבוצת Google Chat. הסקריפט של Apps Script, שרץ כחבר רגיל בקבוצה, קורא את ההודעה בבדיקה המחזורית שלו. אם צריך להעביר לימות, הוא לא פונה לימות ישירות, כי זה חסום, אלא שולח את התוכן ל-Cloudflare Worker. ה-Worker, שיושב במקום שמורשה לדבר עם ימות, מעלה את הטקסט כקובץ TTS לשלוחה. מחייגים לקו ושומעים את ההודעות מוקראות. שום חלק בשרשרת לא דורש חשבון עסקי, ושום חלק לא שולח שיחות יוצאות, אז אין שום סיכון של חיוג לא רצוי.
      בהצלחה רבה!

      פורסם בפורום מפתחים API
      ד
      דורש 3
    • RE: תיכנות + ממשק טלפוני לאינטרנט + ממשק כרטיסי מתנה

      @עצי-מן אמר בתיכנות + ממשק טלפוני לאינטרנט + ממשק כרטיסי מתנה:

      באיזה אימייל אפשר לפנות לימות המשיח לבקש שיפתחו את זה.

      https://www.yemot.co.il/contact.html

      פורסם בבקשות לפיתוח
      ד
      דורש 3
    • RE: פוסט תגובות על המודולים מבית @איש יהודי טוב

      @אנוכי אי אפשר להתחבר אתה רק מקבל מייל שהורחקת ומאז שאתה מנסה להתחבר אתה כאילו לא קיים

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3
    • RE: מערכת לחלוקת חדרי ועידה בחינם

      @קול-הקווים אמר במערכת לחלוקת חדרי ועידה בחינם:

      @yonatan עדיין נשאר.??

      כן

      פורסם בטיפים עצות והדגמות מהמשתמשים
      ד
      דורש 3
    • RE: מערכת לחלוקת חדרי ועידה בחינם

      @yonatan אמר במערכת לחלוקת חדרי ועידה בחינם:

      @דורש-3 הרגע. תגיד 0733 זה מספר כשר?

      כן זה פרייבט

      פורסם בטיפים עצות והדגמות מהמשתמשים
      ד
      דורש 3
    • RE: מערכת לחלוקת חדרי ועידה בחינם

      @yonatan אמר במערכת לחלוקת חדרי ועידה בחינם:

      @דורש-3 אני באמצע פתיחה, אבל הוא פותח לי IVR ולא פרייבט! ור' ברוך בער כתב שזה רק פרייבט כשר

      אני קבלתי מספר 073 כשר פרייבט

      פורסם בטיפים עצות והדגמות מהמשתמשים
      ד
      דורש 3
    • RE: מערכת לחלוקת חדרי ועידה בחינם

      @yonatan כבר קיבלת מספר?

      פורסם בטיפים עצות והדגמות מהמשתמשים
      ד
      דורש 3
    • RE: מערכת לחלוקת חדרי ועידה בחינם

      @yonatan כן 3 ואז 1

      פורסם בטיפים עצות והדגמות מהמשתמשים
      ד
      דורש 3
    • RE: הודעת ערב טוב צהרים טובים לפי שעות

      @חיים-פרידמן אמר בהודעת ערב טוב צהרים טובים לפי שעות:

      @דורש-3 אני מעדיף בתשלום של יחידות

      אולי אחד מחברי הפרום

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ד
      דורש 3
    • RE: הודעת ערב טוב צהרים טובים לפי שעות

      @חיים-פרידמן יש אפשרות בתשלום אצל ימות

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ד
      דורש 3
    • RE: הודעת ערב טוב צהרים טובים לפי שעות

      @חיים-פרידמן https://f2.freeivr.co.il/topic/1104/איחולי-בוקר-צוהריים-ערב-לילה-טוב-בשלוחת-תפריט

      פורסם בעזרה הדדית למשתמשים מתקדמים
      ד
      דורש 3
    • RE: חדר וועידה

      @משה-כהן-2 https://f2.freeivr.co.il/topic/3831/הוראות-להפעלת-הממשק-ניהול-ושליטה-על-חדרי-הועידה-החדש?_=1612635912317

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3
    • RE: טלפון נוסף למערכת

      @יאיר-32 שהיה בהצלחה רבה

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3
    • RE: מישהוא יודע למה זה עשה לי את זה?

      @אהובה-ארליך-0 תעשי F5

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3
    • RE: התפריט של ימות

      @רבי-יצחק אבל זה לא נשמע טוב עוד לא הספקת ללחוץ והא כבר חוזר על עצמו

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3
    • RE: התפריט של ימות

      @שנייני בדקתי וזה עדין לא לגמרי מושלם

      פורסם בשאלות ועזרה הדדית
      ד
      דורש 3