رفتن به متن
هم‌سخن
برای برنامه‌نویس‌ها

هم‌سخن را به سایتتان وصل کنید

هم‌سخن با یک تگ <script> روی هر سایتی می‌نشیند: وردپرس، Laravel، Nuxt، Next.js، یا HTML ساده. این صفحه هر چیزی را که برنامه‌نویس سایت لازم دارد توضیح می‌دهد: نصب، API جاوااسکریپت، شناسایی کاربر، اتصال اطلاعات مشتری، و عیب‌یابی.

آدرس سرویس در این صفحه https://hamsokhan.shop است. · تغییرات

مفاهیم

مفهومیعنی
فضای کاری (workspace)یک تیم با اعضا، صندوق گفتگو، پاسخ‌های آماده و سؤالات متداول مشترک. داده‌ی هر فضای کاری کاملاً از بقیه جداست.
سایت (site)هر دامنه‌ای که ویجت رویش نصب می‌شود. هر فضای کاری می‌تواند چند سایت داشته باشد و هر سایت ظاهر و قوانین خودش را دارد.
کلید عمومی pk_…شناسه‌ی سایت در کد نصب. دیده شدنش در صفحه اشکالی ندارد.
کلید محرمانه sk_…فقط روی سرور شما. با آن هویت کاربر را امضا می‌کنید و درخواست‌های هم‌سخن را بررسی می‌کنید. فقط یک بار نمایش داده می‌شود (هنگام ساخت سایت یا ساخت کلید جدید).
بازدیدکننده و مخاطبهر مرورگر یک بازدیدکننده است. مخاطب، آدمی است که گفتگو می‌کند. با شناسایی، بازدیدکننده‌های چند دستگاه یک مخاطب می‌شوند.
گفتگو (conversation)رشته‌ی پیام‌های یک مخاطب. یا باز است یا بسته؛ پیام بعد از بسته شدن، گفتگوی تازه باز می‌کند.

نصب

۱. کد نصب

این کد را در <head> همه‌ی صفحه‌ها بگذارید. کلید pk_… را از پنل بردارید: سایت‌ها ← سایت شما ← نصب و امنیت. همان‌جا کد آماده با کلید خودتان هم هست.

HTML
<script>
  window.Livechat = window.Livechat || { q: [] };
  ['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach(function (m) {
    window.Livechat[m] = window.Livechat[m] || function () { window.Livechat.q.push([m].concat([].slice.call(arguments))); };
  });
</script>
<script async src="https://hamsokhan.shop/loader.js" data-site-key="pk_YOUR_SITE_KEY"></script>

اسکریپت اول یک صف کوچک می‌سازد تا بتوانید Livechat.* را حتی قبل از دانلود loader صدا بزنید. loader بعد از رسیدن، همه را به ترتیب اجرا می‌کند.

تب نصب و امنیت با کد نصب، دامنه‌های مجاز، پیش‌نمایش نوشتن، اطلاعات مشتری و کلید محرمانه
  1. کد نصب با کلید سایت شما؛ دکمه‌ی «کپی» کنارش است.
  2. دامنه‌های مجاز: هر origin در یک خط، مثل https://kafshland.example. ویجت روی هیچ دامنه‌ی دیگری باز نمی‌شود.
  3. آدرس Context Provider و دکمه‌ی تست.
  4. ساخت کلید محرمانه‌ی جدید. کلید قبلی بلافاصله باطل می‌شود.

۲. دامنه‌های مجاز

هر آدرسی که ویجت در آن نمایش داده می‌شود باید در «دامنه‌های مجاز» باشد، دقیقاً به شکل origin: پروتکل و دامنه و پورت، بدون مسیر.

درستنادرست
https://kafshland.examplekafshland.example (بدون پروتکل)
https://www.kafshland.examplehttps://kafshland.example/shop (مسیر دارد)
http://localhost:3000 (برای توسعه)دامنه‌ی فارسی بدون punycode

این محدودیت با frame-ancestors روی صفحه‌ی چت اعمال می‌شود؛ مرورگر چت را در سایت‌های دیگر نشان نمی‌دهد.

۳. Content-Security-Policy و Permissions-Policy

اگر سایتتان CSP دارد، دامنه‌ی هم‌سخن را به این سه اضافه کنید:

HTTP header
Content-Security-Policy: … script-src … https://hamsokhan.shop; connect-src … https://hamsokhan.shop; frame-src … https://hamsokhan.shop

برای پیام صوتی، اگر Permissions-Policy میکروفون را بسته است (microphone=())، به قاب چت اجازه بدهید:

nginx
add_header Permissions-Policy 'geolocation=(), microphone=("https://hamsokhan.shop"), camera=()' always;

بقیه‌ی کارهای چت (WebSocket، فایل‌ها، فونت) داخل قاب خود هم‌سخن انجام می‌شود و به CSP سایت شما ربطی ندارد. اگر میکروفون بسته باشد، دکمه‌ی ضبط به‌جای خطا دادن پنهان می‌شود.

۴. در فریم‌ورک‌ها

در اپ‌های تک‌صفحه‌ای (SPA) کد را فقط یک بار اضافه کنید. تغییر مسیر با pushState، replaceState، popstate و hashchange خودکار تشخیص داده می‌شود و قوانین نمایش دوباره بررسی می‌شوند.

plugins/livechat.client.ts
export default defineNuxtPlugin(() => {
  const { origin, siteKey } = useRuntimeConfig().public.livechat;
  if (!origin || !siteKey) return;

  const w = window as any;
  w.Livechat = w.Livechat || { q: [] };
  ['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach((m) => {
    w.Livechat[m] = w.Livechat[m] || ((...args: unknown[]) => w.Livechat.q.push([m, ...args]));
  });

  // After the page has loaded, so the chat never competes with the page itself.
  const add = () => {
    const script = document.createElement('script');
    script.async = true;
    script.src = `${origin}/loader.js`;
    script.dataset.siteKey = siteKey;
    document.head.append(script);
  };
  if (document.readyState === 'complete') add();
  else window.addEventListener('load', add, { once: true });
});
components/Livechat.tsx
'use client';
import { useEffect } from 'react';

export function Livechat({ siteKey }: { siteKey: string }) {
  useEffect(() => {
    const w = window as any;
    if (w.Livechat?.loaded) return;
    w.Livechat = w.Livechat || { q: [] };
    ['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach((m) => {
      w.Livechat[m] = w.Livechat[m] || ((...args: unknown[]) => w.Livechat.q.push([m, ...args]));
    });
    const script = document.createElement('script');
    script.async = true;
    script.src = 'https://hamsokhan.shop/loader.js';
    script.dataset.siteKey = siteKey;
    document.head.append(script);
    w.Livechat.loaded = true;
  }, [siteKey]);
  return null;
}

// app/layout.tsx:  <Livechat siteKey="pk_YOUR_SITE_KEY" />
functions.php
add_action('wp_head', function () {
    ?>
    <script>
      window.Livechat = window.Livechat || { q: [] };
      ['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach(function (m) {
        window.Livechat[m] = window.Livechat[m] || function () { window.Livechat.q.push([m].concat([].slice.call(arguments))); };
      });
    </script>
    <script async src="https://hamsokhan.shop/loader.js" data-site-key="pk_YOUR_SITE_KEY"></script>
    <?php
});
resources/views/layouts/app.blade.php
@if (config('services.hamsokhan.site_key'))
  <script>
    window.Livechat = window.Livechat || { q: [] };
    ['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach(function (m) {
      window.Livechat[m] = window.Livechat[m] || function () { window.Livechat.q.push([m].concat([].slice.call(arguments))); };
    });
  </script>
  <script async src="https://hamsokhan.shop/loader.js" data-site-key="{{ config('services.hamsokhan.site_key') }}"></script>
@endif

کارایی

  • loader حدود ۶ کیلوبایت (gzip) است و async دانلود می‌شود؛ رندر صفحه را متوقف نمی‌کند.
  • خود چت (Vue، حدود ۹۰ کیلوبایت) در یک iframe و فقط وقتی مرورگر بیکار است یا بازدیدکننده چت را باز می‌کند بار می‌شود.
  • دکمه‌ی چت جای ثابت دارد و جابه‌جایی صفحه (CLS) ایجاد نمی‌کند.
  • اگر سرور هم‌سخن در دسترس نباشد، فقط دکمه‌ی چت نمی‌آید؛ صفحه‌ی شما هیچ خطایی نمی‌دهد.

API جاوااسکریپت

همه‌ی متدها روی window.Livechat هستند و قبل از رسیدن loader هم در صف می‌مانند.

متدها

متدکار
open() · close() · toggle()باز و بسته کردن پنجره‌ی چت.
isOpen()اگر چت باز است true برمی‌گرداند (فقط بعد از رسیدن loader).
setUser({ id, hash, name?, email?, phone?, traits? })شناسایی کاربر واردشده. بدون hash معتبر، کاربر ناشناس می‌ماند. جزئیات
logout()فراموش کردن کاربر و شروع یک نشست ناشناس تازه. بعد از خروج کاربر از سایت صدا بزنید.
setContext({ … })اطلاعاتی برای قوانین نمایش، مثل { cart_count: 2 }. جزئیات
showMessage(text)یک پیام محلی در ویجت نشان می‌دهد؛ ذخیره نمی‌شود و به اپراتور نمی‌رسد.
trackPage()بررسی دوباره‌ی قوانین بعد از تغییر مسیر. معمولاً لازم نیست.
on(event, callback) · off(event, callback)گوش دادن به رویدادها.
destroy()ویجت را کامل از صفحه برمی‌دارد.
مثال
// A "need help?" link that opens the chat with a hint for the visitor.
document.querySelector('#help').addEventListener('click', (event) => {
  event.preventDefault();
  Livechat.open();
  Livechat.showMessage('سفارش شما ثبت شده؛ برای پیگیری کد سفارش را بنویسید.');
});

// Display rules can use anything the page knows.
Livechat.setContext({ cart_count: cart.items.length, plan: 'gold' });

رویدادها

رویداددادهکی
ready–ویجت آماده است.
open · close–چت باز یا بسته شد.
unreadتعدادتعداد پیام‌های خوانده‌نشده عوض شد.
message:sent{ body }بازدیدکننده پیامی فرستاد.
message:received{ body, sender }پیامی از اپراتور یا دستیار هوشمند رسید؛ sender نام فرستنده است (برای ربات، نامی که در تنظیمات سایت گذاشته‌اید).
form:submitted{ kind }فرم شروع یا آفلاین ارسال شد.
rating{ score }بازدیدکننده از ۱ تا ۵ امتیاز داد.
flow:option{ flow, option, label }گزینه‌ای در گفتگوی آماده انتخاب شد.
نام دلخواه{ flow, option }گزینه‌ای با کار «رویداد» در گفتگوی آماده انتخاب شد.
مثال
// Count conversations in your analytics.
Livechat.on('message:sent', () => gtag('event', 'chat_message'));

// A flow option with action "event" named "show-size-guide" opens your own size guide.
Livechat.on('show-size-guide', () => openSizeGuideModal());

// Badge on your own icon.
Livechat.on('unread', (count) => { badge.textContent = count || ''; });

دکمه و شمارنده‌ی خود سایت، بدون جاوااسکریپت

loader دو ویژگی HTML را هر جای صفحه پیدا می‌کند، حتی روی المان‌هایی که بعداً اضافه شوند:

  • data-hamsokhan-open: کلیک روی المان چت را باز می‌کند (و اگر باز است، می‌بندد).
  • data-hamsokhan-counter: متن المان تعداد پیام‌های خوانده‌نشده می‌شود (با ارقام زبان ویجت)، یا خالی وقتی پیامی نیست.
HTML
<button type="button" data-hamsokhan-open>
  گفتگو با ما <span class="badge" data-hamsokhan-counter></span>
</button>

<style>
  .badge:empty { display: none; }
</style>

برای این‌که فقط دکمه‌ی خودتان دیده شود، در پنل «نمایش دکمه‌ی چت» را خاموش کنید (ظاهر ویجت). پنجره‌ی چت همچنان از گوشه‌ی صفحه باز می‌شود.

شناسایی کاربر (Identity Verification)

اگر سایتتان حساب کاربری دارد، این بخش را حتماً انجام دهید.

چرا امضا لازم است؟

اگر هم‌سخن هر idی را از مرورگر قبول می‌کرد، هر کسی می‌توانست در Console بنویسد setUser({ id: '42' }) و گفتگوهای مشتری شماره‌ی ۴۲ را بخواند. برای همین شناسه باید با کلید محرمانه‌ای امضا شود که فقط روی سرور شماست.

روی سرور: امضا

قاعده
hash = hex( HMAC-SHA256( message = String(user_id), key = site_secret ) )

یک endpoint بسازید که فقط به کاربر واردشده جواب بدهد:

routes/api.php
Route::middleware('auth:sanctum')->get('/livechat/identity', function (Request $request) {
    $user = $request->user();
    $id = (string) $user->id;

    return [
        'id' => $id,
        'hash' => hash_hmac('sha256', $id, config('services.hamsokhan.site_secret')),
        'name' => $user->name,
        'email' => $user->email,
        'phone' => $user->mobile,
    ];
});

// config/services.php
'hamsokhan' => ['site_secret' => env('HAMSOKHAN_SITE_SECRET')],
Express
import { createHmac } from 'node:crypto';

app.get('/livechat/identity', requireLogin, (req, res) => {
  const id = String(req.user.id);
  res.json({
    id,
    hash: createHmac('sha256', process.env.HAMSOKHAN_SITE_SECRET).update(id).digest('hex'),
    name: req.user.name,
    email: req.user.email,
  });
});
Django
import hashlib, hmac, os
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse

@login_required
def livechat_identity(request):
    user_id = str(request.user.id)
    digest = hmac.new(os.environ['HAMSOKHAN_SITE_SECRET'].encode(), user_id.encode(), hashlib.sha256).hexdigest()
    return JsonResponse({'id': user_id, 'hash': digest, 'name': request.user.get_full_name(), 'email': request.user.email})

در صفحه: setUser و logout

JavaScript
// After login, or on page load when the user is already logged in.
const response = await fetch('/api/livechat/identity', { credentials: 'include' });
if (response.ok) Livechat.setUser(await response.json());

// Optional: facts for the team, shown beside the conversation. null removes one.
Livechat.setUser({ ...identity, traits: { plan: 'gold', orders: 12 } });

// On logout, so the next person on this browser does not see the conversation.
Livechat.logout();

قاعده‌ها

  • id شناسه‌ی ثابت کاربر در سایت شماست و باید رشته باشد. ایمیل یا موبایل نه، چون عوض می‌شوند.
  • بدون hash درست، کاربر ناشناس می‌ماند و name و بقیه فقط نمایشی‌اند.
  • با شناسایی، گفتگوهای ناشناس همان مرورگر به حساب کاربر منتقل می‌شوند. ورود با حساب دیگر هرگز تاریخچه‌ی کاربر قبلی را منتقل نمی‌کند.
  • امضا را در هر بار لود صفحه بگیرید و جایی کش نکنید. اگر کلید عوض شود، امضاهای قبلی باطل‌اند.
  • traits تا ۳۰ مورد؛ نام تا ۴۰ و مقدار تا ۲۵۵ نویسه. هر فراخوانی با قبلی ادغام می‌شود.
⚠️ کلید محرمانه

sk_… فقط در متغیرهای محیطی سرور. هرگز در کد سمت مرورگر، فایل‌های .env فرانت‌اند (مثل NUXT_PUBLIC_* یا NEXT_PUBLIC_*)، یا git.

قوانین نمایش و setContext

قوانینی که مدیر در پنل می‌سازد (نمایش ویجت، پیام خودکار، گفتگوی آماده، پیشنهاد مقاله، مسیریابی) روی این «واقعیت‌ها» کار می‌کنند. loader بیشترشان را خودش جمع می‌کند؛ شما فقط context.* را با setContext می‌فرستید.

فیلدنوعتوضیح
urlمتنآدرس کامل صفحه
deviceمتنmobile، tablet یا desktop
statusمتنonline یا offline
time_on_pageعددثانیه
visit_countعددتعداد بازدیدهای این مرورگر
identifiedبله/خیرکاربر با امضا شناسایی شده
has_conversationبله/خیرگفتگوی باز دارد
languageمتنزبان صفحه، مثلاً fa
utm_source · utm_medium · utm_campaignمتناز آدرس ورود
context.*هر چیزهر چه با setContext فرستاده‌اید

عملگرها

عملگربرایرفتار
contains · equals · starts_with · ends_withمتنبدون حساسیت به حروف بزرگ و کوچک
globمتن* هر چیزی (حتی /)، ? یک نویسه؛ کل مقدار باید جور باشد
regexمتنحداکثر ۲۰۰ نویسه؛ فقط چیزهایی که هم در JavaScript و هم در PCRE کار می‌کنند
eq · neq · in · not_inمقدار سادهبرابری دقیق
gt · gte · lt · lteعددهر دو طرف باید عدد باشند

مقایسه‌ی آدرس‌ها

پیش از مقایسه، هم آدرس صفحه و هم مقدار قانون یکدست می‌شوند: #… و query حذف می‌شوند (مگر قانون with_query داشته باشد)، مسیر decode می‌شود (پس /product/کفش و نسخه‌ی percent-encoded آن یکی‌اند)، اسلش آخر حذف می‌شود، و www. نادیده گرفته می‌شود. مقداری که با / شروع شود با مسیر مقایسه می‌شود و بقیه با «دامنه + مسیر».

اطلاعات مشتری از سایت (Context Provider)

یک endpoint روی سرور شما که هم‌سخن برای مشتری‌های شناسایی‌شده صدا می‌زند. جوابش به شکل کارت کنار گفتگو نمایش داده می‌شود: سبد خرید، سفارش‌ها، کیف پول، یا هر چیز دیگر. هیچ‌کدام در هم‌سخن ذخیره نمی‌شود.

درخواست

HTTP
POST https://your-site.example/api/livechat/context
Content-Type: application/json
User-Agent: Hamsokhan-Context/1
X-Hamsokhan-Timestamp: 1761300000
X-Hamsokhan-Signature: sha256=5c3f…

{
  "event": "conversation.context",
  "site_id": "01m5…",
  "conversation_id": "01m5…",
  "external_user_id": "42",
  "contact": { "name": "علی رضایی", "email": null, "phone": "09120000000" },
  "page": "https://your-site.example/cart"
}
  • external_user_id همان idی است که به setUser داده‌اید.
  • وقتی مدیر در پنل «تست» را می‌زند، event برابر context.test و conversation_id برابر null است.

بررسی امضا

قاعده
X-Hamsokhan-Signature = "sha256=" + hex( HMAC-SHA256( timestamp + "." + raw_body, site_secret ) )

کلید همان sk_… است. درخواست را رد کنید مگر این‌که امضا روی بدنه‌ی خام (همان بایت‌هایی که رسیده) درست باشد و زمان حداکثر ۵ دقیقه با ساعت سرور فاصله داشته باشد. مقایسه را با تابع زمان‌ثابت انجام دهید.

app/Http/Middleware/VerifyHamsokhanSignature.php
public function handle(Request $request, Closure $next): Response
{
    $secret = (string) config('services.hamsokhan.site_secret');
    $timestamp = (string) $request->header('X-Hamsokhan-Timestamp');
    $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$request->getContent(), $secret);

    abort_unless(
        $secret !== ''
            && ctype_digit($timestamp)
            && abs(time() - (int) $timestamp) <= 300
            && hash_equals($expected, (string) $request->header('X-Hamsokhan-Signature')),
        401,
    );

    return $next($request);
}
Express
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/api/livechat/context', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestamp = req.get('X-Hamsokhan-Timestamp') ?? '';
  const expected = Buffer.from(`sha256=${createHmac('sha256', process.env.HAMSOKHAN_SITE_SECRET).update(`${timestamp}.${req.body}`).digest('hex')}`);
  const given = Buffer.from(req.get('X-Hamsokhan-Signature') ?? '');
  const fresh = /^\d+$/.test(timestamp) && Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  if (!fresh || given.length !== expected.length || !timingSafeEqual(given, expected)) return res.sendStatus(401);

  const { external_user_id: userId } = JSON.parse(req.body);
  const user = await users.find(userId);
  if (!user) return res.sendStatus(404);
  res.json({ cards: await cardsFor(user) });
});
Flask
import hashlib, hmac, json, os, time
from flask import abort, request

@app.post('/api/livechat/context')
def livechat_context():
    timestamp = request.headers.get('X-Hamsokhan-Timestamp', '')
    raw = request.get_data()
    expected = 'sha256=' + hmac.new(os.environ['HAMSOKHAN_SITE_SECRET'].encode(), timestamp.encode() + b'.' + raw, hashlib.sha256).hexdigest()
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300 \
            or not hmac.compare_digest(expected, request.headers.get('X-Hamsokhan-Signature', '')):
        abort(401)
    user = find_user(json.loads(raw)['external_user_id']) or abort(404)
    return {'cards': cards_for(user)}

جواب

200 با حداکثر ۸ کارت. هر کارت یک title و دست‌کم یک fields یا items دارد:

JSON
{
  "cards": [
    {
      "title": "سبد خرید",
      "fields": [
        { "label": "تعداد", "value": "۲ کالا" },
        { "label": "جمع", "value": "۱٬۲۵۰٬۰۰۰ تومان" }
      ],
      "actions": [{ "label": "مشاهده در پنل", "url": "https://your-site.example/admin/carts/17" }]
    },
    {
      "title": "سفارش‌های اخیر",
      "items": [
        { "title": "#1042", "subtitle": "ارسال شده · کد رهگیری 1234", "meta": "۳ آبان", "url": "https://your-site.example/admin/orders/1042" }
      ]
    }
  ]
}
بخشحد
کارت‌ها۸، هر کدام با title تا ۸۰ نویسه
fields۱۲ در هر کارت: label (۶۰)، value (۲۰۰)
items۱۰ در هر کارت: title (۱۲۰)، اختیاری subtitle (۲۰۰)، meta (۶۰)، url
actions۳ در هر کارت: label (۴۰)، url
کل جواب۲۵۶ کیلوبایت
  • همه‌چیز متن ساده نمایش داده می‌شود و HTML اجرا نمی‌شود. فقط لینک‌های http و https نگه داشته می‌شوند. هر چیز بیرون از این شکل یا حدها، بی‌صدا کنار گذاشته می‌شود.
  • 404 یعنی «این کاربر را نداریم» و کارتی نشان داده نمی‌شود. هر وضعیت دیگر، timeout یا جواب نامعتبر، به اپراتور می‌گوید اطلاعات سایت در دسترس نیست.

رفتار

  • timeout: ۲ ثانیه، بدون تلاش دوباره و بدون دنبال کردن redirect. endpoint را سبک نگه دارید؛ فقط چیزی را بخوانید که کارت‌ها نشان می‌دهند.
  • کش: جواب ۶۰ ثانیه برای هر مشتری و خطا ۱۵ ثانیه. اپراتور با دکمه‌ی تازه‌سازی دوباره می‌پرسد.
  • شبکه: آدرس باید به یک IP عمومی برسد. اگر پشت CDN یا فایروال هستید، User-Agent Hamsokhan-Context/1 را مسدود نکنید.
  • تست: در پنل، «نصب و امنیت ← اطلاعات مشتری از سایت»، آدرس را بنویسید و با شناسه‌ی یک کاربر واقعی «تست» بزنید.

صفحه‌ی چت مستقل، CSS و صدا

صفحه‌ی چت مستقل

با روشن کردن «صفحه‌ی چت مستقل» در تب «پیشرفته»، آدرس https://hamsokhan.shop/chat/pk_… چت را تمام‌صفحه باز می‌کند. ظاهر، فرم‌ها، گفتگوهای آماده و ساعات کاری همان سایت را دارد. این صفحه در iframe باز نمی‌شود و تا وقتی خاموش است، 404 می‌دهد.

CSS دلخواه

CSS فقط داخل پنجره‌ی چت اعمال می‌شود. کلاس‌های ویجت با hs- شروع می‌شوند:

کلاسبخش
.hs-headerهدر رنگی بالای پنجره
.hs-home · .hs-home__heroصفحه‌ی خانه
.hs-msg · .hs-msg--mine · .hs-msg--theirs · .hs-bubbleپیام‌ها
.hs-composer · .hs-sendجای نوشتن و دکمه‌ی ارسال
.hs-faq__list · .hs-markdownسؤالات متداول و متن مقاله
.hs-form__field · .hs-ratingفرم‌ها و کارت امتیاز
CSS
.hs-header { background: linear-gradient(135deg, #0f766e, #14b8a6); }
.hs-msg--mine .hs-bubble { border-radius: 18px 18px 4px 18px; }

CSS از دامنه‌ی هم‌سخن سرو می‌شود و CSP پنجره‌ی چت عکس، فونت یا فایل از دامنه‌های دیگر را بار نمی‌کند. نام کلاس‌ها ممکن است در نسخه‌های بعد عوض شود؛ تغییرات در بخش تغییرات می‌آید.

صدای پیام

mp3، ogg، wav یا m4a تا ۳۰۰ کیلوبایت، از تب «پیشرفته». نوع فایل از روی محتوا بررسی می‌شود.

حدها

چهحد
پیام متنی۵۰۰۰ نویسه
فایل۱۰ مگابایت: JPEG، PNG، WebP، GIF، PDF، متن، DOCX، XLSX، و صدای OGG، MP3، M4A، WebM. تشخیص از روی محتوا.
پیام صوتی۵ دقیقه
عکسحداکثر ۲۵ مگاپیکسل؛ ضلع بلند به ۲۵۶۰ پیکسل کوچک و اطلاعات EXIF پاک می‌شود.
ارسال پیام۲۰ در دقیقه برای هر بازدیدکننده، ۱۲۰ برای هر IP
آپلود۱۰ در دقیقه برای هر بازدیدکننده، ۳۰ برای هر IP
شروع نشست و setUser۳۰ در دقیقه برای هر IP

مدل امنیتی

  • جداسازی: چت در iframe از دامنه‌ی هم‌سخن اجرا می‌شود. اسکریپت‌های سایت شما به گفتگوها و توکن بازدیدکننده دسترسی ندارند. loader فقط دکمه و پیام خودکار را در صفحه می‌سازد.
  • دامنه‌ها: frame-ancestors قاب چت فقط دامنه‌های مجاز سایت است.
  • هویت: بدون امضای HMAC هیچ شناسه‌ای پذیرفته نمی‌شود. مقایسه‌ها زمان‌ثابت‌اند.
  • ذخیره در مرورگر: روی سایت شما فقط شمارنده‌ی بازدید و UTM ورود (کلیدهای hamsokhan:*). توکن بازدیدکننده داخل iframe است و مرورگرها آن را برای هر سایت جدا نگه می‌دارند.
  • محتوا: پیام‌ها متن ساده‌اند و لینک‌ها امن ساخته می‌شوند. متن مقاله‌ها Markdown محدود است و HTML اجرا نمی‌شود.
  • بدون کوکی: API ویجت کوکی ندارد، پس CSRF معنی ندارد.

عیب‌یابی

دکمه‌ی چت نمی‌آید
  1. Console مرورگر: خطای CSP درباره‌ی hamsokhan.shop؟ CSP را اصلاح کنید.
  2. Network: loader.js با 200 آمده؟ data-site-key درست است؟
  3. صفحه‌ی چت در Console می‌گوید «refused to frame»؟ origin صفحه در دامنه‌های مجاز نیست (www، پورت و https را چک کنید).
  4. قوانین نمایش، حالت آفلاین «مخفی» یا «نمایش در موبایل» ویجت را پنهان نکرده باشند.
کاربر بعد از setUser هنوز ناشناس است
  • id را دقیقاً همان رشته‌ای بدهید که امضا کرده‌اید ("42" و 42 فرق دارند؛ رشته بفرستید).
  • کلید سرور همان کلید فعلی سایت است؟ بعد از «ساخت کلید جدید» باید سرور را هم به‌روز کنید.
  • در Laravel اگر config:cache دارید، بعد از تغییر .env کش را دوباره بسازید.
تست Context Provider خطا می‌دهد
خطاعلت محتمل
401امضا روی بدنه‌ی پردازش‌شده حساب شده نه خام، کلید اشتباه است، یا ساعت سرور بیش از ۵ دقیقه جلو یا عقب است.
timeoutendpoint بیش از ۲ ثانیه طول کشیده، یا CDN و فایروال درخواست را نگه داشته‌اند.
private_networkآدرس به IP خصوصی می‌رسد؛ آدرس عمومی بدهید.
not_json یا کارتی نمی‌آیدجواب باید {"cards": [...]} با Content-Type: application/json باشد.
دکمه‌ی میکروفون نیست

صفحه باید HTTPS باشد و Permissions-Policy سایت میکروفون را برای https://hamsokhan.shop باز گذاشته باشد. نمونه

بعد از تغییر مسیر در SPA قوانین اعمال نمی‌شوند

اگر روتر شما از history استاندارد استفاده نمی‌کند، بعد از هر تغییر مسیر Livechat.trackPage() را صدا بزنید.

تغییرات

    • نسخه‌ی اول: کد نصب، API جاوااسکریپت، شناسایی کاربر و قوانین نمایش.