در اولین آموزش Flarum – “Writing a Flarum Extension: Building a Custom Field” – ما نحوه افزودن یک فیلد سفارشی جدید به پروفایل کاربر را در یک نرم افزار فروم منبع باز سریع و بسیار گسترده به نام فوت کردن، دمیدن. زمینه ای که ما اضافه کردیم web3address، حساب هویت Web3 کاربر است.

در این آموزش دوم ، با اجازه دادن به کاربران برای افزودن آدرس Web3 به نمایه خود ، کارها را یک گام فراتر می بریم.

توجه ℹ: اکوسیستم Web3 اینترنت جدیدی از میزبانی غیرمتمرکز ، داده های متعلق به خود و ارتباطات مقاوم در برابر سانسور است. لطفاً برای یک آغازگر در Web3 مراجعه کنید این صحبت 15 دقیقه ای در FOSDEM.

رمزنگاری اضافه کردن Web3

در این مرحله ، زمینه سفارشی ما کار می کند ، توسط کاربران قابل تنظیم است و توسط مدیران انجمن قابل ویرایش است. حال بیایید اطمینان حاصل کنیم که کاربران می توانند آدرس خود را به روشی کاملاً رمزنگاری شده اضافه کنند.

این بدان معناست که کاربر فقط قادر است آدرسی را که متعلق به خود است ثابت کند. شما با امضای پیامی با کلید خصوصی آن آدرس ، مالکیت یک آدرس را ثابت می کنید. فقط شخصی که دارای کلید خصوصی یک کلید اصلی عمومی-خصوصی است مالک آن صفحه کلید محسوب می شود. قسمت عمومی یک کلید جفت عمومی-خصوصی پایه ای است که از طریق آن آدرس Web3 به صورت ریاضی مشتق می شود.

برای داشتن برخی از آدرس ها ، کاربر باید آدرس را نصب کند پسوند Polkadot JS و ایجاد یک حساب کاربری رابط کاربری باید خود توضیح داده شود ، اما یک راهنمای دقیق تر وجود دارد اینجا در صورت نیاز

فیلدی که ما در آموزش قبلی اضافه کردیم در حال حاضر به کاربران اجازه می دهد تا هر مقداری را به صورت دستی تنظیم کنند ، اما این بدان معناست که کاربران می توانند آدرس هرکسی را وارد کنند یا حتی برخی از موارد مهم را وارد کنید. ما می خواهیم آنها فقط دکمه های خود را اضافه کنند ، بنابراین دکمه ای را جایگزین می کنیم که:

  • اجازه دسترسی به پسوند مرورگر حاوی حساب (ها) را بخواهید
  • حسابها را بارگیری کنید و برای انتخاب یکی از آنها یک لیست کشویی ارائه دهید
  • از کاربر بخواهید پیامی را با آن آدرس امضا کند و آن امضا را تأیید کند
  • آن حساب را به عنوان آدرس Web3 کاربر ثبت کنید

بیایید غواصی کنیم

دکمه

ابتدا باید فیلد ورودی Web3 خود را به a تغییر دهیم کشویی. بیایید ایجاد کنیم components/Web3Dropdown.js:

import Component from "flarum/Component";
import Dropdown from "flarum/components/Dropdown";

export default class Web3Dropdown extends Component {
  view() {
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Add Web3 Account"
      >
      </Dropdown>
    );
  }

  handleClick(e) {
    console.log("Pick something");
  }
}

ما یک م componentلفه جدید به سبک. ایجاد می کنیم Web3Field.js ما قبلاً ایجاد کردیم ، اما اکنون نمونه ای از م Dلفه Dropdown را برمی گردانیم. جز D Dropdown یکی از چندین جز standard استاندارد JS در Flarum است. می توانید لیست کاملی پیدا کنید اینجا. ما همچنین کلاس “دکمه” را به آن می دهیم تا سبک آن با سایر بخشهای انجمن مطابقت داشته باشد. با کلیک ، ما یک پیام را چاپ می کنیم.

این م aلفه دکمه ای است با قابلیت فراخوانی کشویی از موارد منتقل شده ، دقیقاً مانند منوی “کنترل ها” که یک مدیر انجمن می تواند در نمایه کاربر مشاهده کند:

دکمه ساده

وابستگی ها

در پوشه JS پسوند خود ، دو وابستگی اضافه خواهیم کرد:

yarn add @polkadot/util-crypto @polkadot/util @polkadot/extension-dapp

توجه ⚠: اگر هنوز در حال اجرا هستید ، فرایند را فراموش نکنید yarn dev و فراموش نکنید که پس از نصب این وابستگی ها ، آن را دوباره شروع کنید!

util-crypto شامل برخی از توابع سودمند برای عملیات رمزنگاری است.util شامل برخی از ابزارهای اساسی مانند تبدیل رشته ها به بایت و غیره است (وجود دارد اسناد برای هر دو اینجا.) extension-dapp یک لایه کمکی است که به ما اجازه می دهد JS ما را بنویسیم با پسوند Polkadot JS که نصب کرده ایم تعامل داشته باشد. (بازدید از اسناد اینجا.)

درخواست مجوز و گرفتن حساب

بیایید اکنون Dropdown خود را تغییر دهیم تا از کاربر اجازه دسترسی به حسابهای Web3 خود را بگیریم:

  import { web3Accounts, web3Enable } from "@polkadot/extension-dapp";

  

  async handleClick(e) {
    await web3Enable("Flarum Web3 Address Extension");
    const accounts = await web3Accounts();
    console.log(accounts);
  }

توجه کنید که ما تغییر handleClick عملکرد بودن async! ما به این نیاز داریم تا بتوانیم await وعده ها در کد. در غیر این صورت ، ما با لانه سازی گیر می کنیم then تماس می گیرد.

ابتدا تماس می گیریم web3Enable، که از ما اجازه دسترسی به پسوند را می خواهد. سپس همه حساب های کاربر را گرفته و در کنسول خروجی می دهیم. اگر برنامه افزودنی Polkadot JS را نصب کرده و برخی از حساب ها را بارگیری کرده است ، اکنون سعی کنید این مورد را امتحان کنید.

مجاز یا رد کنید

خروجی در کنسول

اما اگر کسی پسوند را نصب نکند چه می شود؟ ما می توانیم یک تنظیمات سطح مدیر داشته باشیم که به ما اجازه می دهد در صورت عدم وجود پسوند دکمه را مخفی کنیم یا کاربر را به URL خود هدایت کنیم ، اما در حال حاضر ، اجازه دهید مورد دوم را انتخاب کنیم:

  import { web3Accounts, web3Enable, isWeb3Injected } from "@polkadot/extension-dapp";

  

  async handleClick(e) {
    await web3Enable("Flarum Web3 Address Extension");
    if (isWeb3Injected) {
      const accounts = await web3Accounts();
      console.log(accounts);
    } else {
      window.location = "https://github.com/polkadot-js/extension";
    }
  }

انتخاب حساب

مرحله بعدی اجازه دادن به کاربر برای انتخاب حساب کاربری و در عین حال درخواست از او برای امضای پیام با آن است. پس از تأیید ، این امضا به طور غیرقابل انکاری مالکیت حساب را اثبات می کند.

جز D Dropdown طول می کشد items آرایه ای از موارد برای نمایش. این معمولا آرایه ای از است Button عناصر ، کجا دکمه یک جز common رایج Flarum است. برای اینکه به جز component خود یک خاصیت داده در سطح کامپوننت بدهیم که بتوانیم آن را دستکاری کنیم و تغییرات را بر اساس آن تنظیم کنیم ، آن را در تعریف می کنیم oninit:

  oninit() {
    this.web3accounts = [];
  }

به جای فقط console.logدر حال accounts، سپس تنظیم می کنیم accounts به این ویژگی جدید:

this.web3accounts = accounts;
m.redraw();

توجه: ما استفاده می کنیم redraw در اینجا برای ساخت mithril (m) کامپوننت خود را دوباره ارائه کنید. اگر این کار را نکنیم ، مولفه ابتدا یک کرکره کشویی خالی ارائه می دهد (هنوز حساب ندارد) و برای نمایش حساب ها (که باعث ایجاد مجدد قرعه کشی می شود) به باز کردن کرکره باز دیگری نیاز دارد. ما می خواهیم حساب ها را در لیست کشویی قرار دهیم به محض بارگیری آنهاstrong> ، حتی اگر کشویی از قبل باز باشد و هیچ عنصری نداشته باشد ، بنابراین این کار فریب خواهد داد. هر زمان که بخواهید تغییرات را در مولفه خود به صورت پویا و بدون محرک UI اعمال کنید ، معمولاً براساس برخی از واکشی های داده از راه دور یا پردازش داده ها ، استفاده از شما اشکالی ندارد m.redraw().

در آخر ، ما درست می کنیم view، عملکرد مسئول ارائه ما ، نسبت به این تغییر واکنش نشان می دهد:

  view() {
    const items = [];
    if (this.web3accounts.length) {
      for (let i = 0; i < this.web3accounts.length; i++) {
        items.push(
          <Button
            value={this.web3accounts[i].address}
            onclick={this.handleAccountSelect}
          >
            {this.web3accounts[i].address}
            {this.web3accounts[i].meta.name
              ? ` - ${this.web3accounts[i].meta.name}`
              : ""}
          </Button>
        );
      }
    }
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Set Web3 Account"
      >
        {items}
      </Dropdown>
    );
  }

ابتدا یک آرایه مکان دار خالی تعریف می کنیم. پس اگر بیش از صفر باشد web3accounts ذخیره شده در این جز component ، ما از طریق آنها تکرار می کنیم تا برای هر حساب یک دکمه با مقدار تعیین شده به آدرس حساب و برچسب تنظیم شده به ترکیبی از آدرس و برچسب تعریف شده در پسوند ایجاد کنیم. در آخر ، آن دکمه ها را به م Dلفه Dropdown منتقل می کنیم.

همچنین باید م componentلفه دکمه را وارد کنیم:

import Button from "flarum/components/Button";

توجه: توجه داشته باشید که ما الزام آور نیستیم this به هر دکمه onclick مسئول رویداد این بخاطر این است که this زمینه دکمه را به جای کلیک کردن روی دکمه ، به جز parent والدین Dropdown تغییر می دهد و واکشی مقدار دکمه را ساده تر می کند.

در مرحله بعدی ، باید درمورد کلیک کاربر بر روی یکی از آدرس های منو واکنش نشان دهیم:

  handleAccountSelect() {
    console.log(this.value);
  }

آزمایش این نشان می دهد که ما واقعاً می توانیم یک حساب Web3 را از پسوند خود انتخاب کنیم و پس از انتخاب در کنسول وارد سیستم شود.

تنظیم حساب Web3 در صفحه نمایه

نمای از کنسول

تأیید اعتبار

در آخر ، باید از کاربر بخواهیم پیامی را امضا کند. بیایید بگوییم پیام “مالکیت شدید” است. با این کار از آنها خواسته می شود در پنجره پسوند رمز عبور خود را ارسال کرده و پیام امضا شده ای را برگردانند.

اول ، برخی از واردات:

import {
  web3Accounts,
  web3Enable,
  isWeb3Injected,
  web3FromAddress,  
} from "@polkadot/extension-dapp";
import { stringToHex } from "@polkadot/util"; 

web3FromAddress یک روش مفید برای ساخت یک شی Web3 است ، شی standard استاندارد برای تعاملات Web3 ، با آدرس داده شده به عنوان “قهرمان اصلی”. stringToHex برای تبدیل یک رشته به نمایش هگزادسیمال استفاده می شود ، این قالب داده ای است که یک امضا کننده انتظار دارد (بایت):

  async handleAccountSelect() {
    const address = this.value;
    const web3 = await web3FromAddress(address);
    const signer = web3.signer;
    const hexMessage = stringToHex("Extreme ownership");
    try {
      const signed = await signer.signRaw({
        type: "bytes",
        data: hexMessage,
        address: address,
      });
      console.log(signed);
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

ابتدا تابع را به an تبدیل می کنیم async یکی تا بتوانیم استفاده کنیم await. سپس ما یک web3 همانطور که در بالا توضیح داده شد ، از آدرس ما استفاده کنید و امضا کننده را استخراج کنید. Signer یک ابزار رمزنگاری است که به صورت خودکار کلید عمومی را از یک آدرس استخراج کرده و یک پیام داده شده را که در بایت ارائه شده است ، امضا می کند. (این همان چیزی است که ما به آن نیاز داریم hexMessage برای – تبدیل رشته ما به بایت ، که به صورت هگزادسیم نمایش داده می شود.)

تنها راه بدست آوردن signed امضا کردن است؛ هر چیز دیگری باعث پرتاب خطا می شود.

ذخیره حساب

سرانجام ، ما همان روند قبلی را با دنبال می کنیم Web3Field.js – آدرس را وارد کنید save:

  async handleAccountSelect() {
    const address = this.value;
    const web3 = await web3FromAddress(address);
    const signer = web3.signer;
    const hexMessage = stringToHex("Extreme ownership");
    try {
      const signed = await signer.signRaw({
        type: "bytes",
        data: hexMessage,
        address: address,
      });
      console.log(signed);
      const user = app.session.user;
      user
        .save({
          web3address: address,
        })
        .then(() => m.redraw());
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

توجه: اضافه می کنیم m.redraw تا بعد از ذخیره مقدار روی صفحه تازه شود. بازخوانی مجدد جاوا اسکریپت پسوند را بازخوانی می کند و در داده های نمونه کاربری که توسط عملیات ذخیره برگردانده شده خوانده می شود ، در صورت موفقیت آمیز بودن ذخیره ، آدرس به روز شده ما را نشان می دهد.

تأیید سمت سرور

این از امنیت منطقی برخوردار است. حتی اگر کسی اطراف JS ما را هک کند و آدرس Web3 را که متعلق به او نیست وارد کند ، در واقع نمی تواند کار زیادی با آن انجام دهد. آنها فقط می توانند خود را به عنوان شخصی معرفی کنند که نیستند. هنوز هم می توانیم با انجام یک اعتبار سنجی از سمت سرور ، این مسئله را حل کنیم.

از آنجا که در حال حاضر هیچ مجموعه ابزاری برای PHP برای توابع رمزنگاری مورد استفاده ما وجود ندارد ، بنابراین باید خلاق باشیم. به طور خاص ، ما یک اسکریپت خط فرمان در TypeScript می نویسیم که از PHP فراخوانی خواهیم کرد.

که در js/src/forum، ایجاد کنید scripts پوشه را اضافه کرده و پرونده را اضافه کنید verify.js:

let util_crypto = require("@polkadot/util-crypto");

util_crypto
  .cryptoWaitReady()
  .then(() => {
    const verification = util_crypto.signatureVerify(
      process.argv[2], 
      process.argv[3], 
      process.argv[4] 
    );
    if (verification.isValid === true) {
      console.log("OK");
      process.exitCode = 0;
    } else {
      console.error("Verification failed");
      process.exitCode = 1;
    }
  })
  .catch(function (e) {
    console.error(e.message);
    process.exit(1);
  });

بسته ابزارهای رمزنگاری شامل روشهای کمکی برای هر آنچه که ما نیاز داریم. cryptoWaitReady منتظر است تا عملیات رمزگذاری آغاز شود – به ویژه ، sr25519 ، که ما در اینجا از آن استفاده می کنیم ، برای گرم شدن به یک قطعه WASM نیاز دارد. سپس ، ما با استفاده از signatureVerify عملکرد با پردازش استدلال های ارائه شده.

ما می توانیم این را به صورت محلی آزمایش کنیم (مقادیر را از محموله درخواست ذخیره پس از تنظیم آدرس در لیست کشویی یا با امضای دستی پیام “مالکیت شدید” در Polkadot UI):

$ node src/forum/scripts/verify.js "Extreme ownership" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> OK
$ node src/forum/scripts/verify.js "Wrong message" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> Verification failed

اسکریپت تأیید ما کار می کند.

توجه: همان پیام امضا شده توسط همان آدرس هر بار هش متفاوتی ایجاد می کند. روی یکسان بودن آنها حساب نکنید. به عنوان مثال ، این سه محموله “مالکیت شدید” است که توسط همان آدرس 3 بار امضا شده است:

// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x0c837b9a5ba43e92159dc2ff31d38f0e52c27a9a5b30ff359e8f09dc33f75e04e403a1e461f3abb89060d25a7bdbda58a5ff03392acd1aa91f001feb44d92c85"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x3857b37684ee7dfd67304568812db8d5a18a41b2344b15112266785da7741963bdd02bb3fd92ba78f9f6d5feae5a61cd7f9650f3de977de159902a52ef27d081"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0xa66438594adfbe72cca60de5c96255edcfd4210a8b5b306e28d7e5ac8fbad86849311333cdba49ab96de1955a69e28278fb9d71076a2007e770627a9664f4a86"}""

ما همچنین باید خود را اصلاح کنیم app.session.user.save تماس بگیرید در Dropdown م componentلفه ، بنابراین در واقع پیام امضا شده را به قسمت انتهایی ارسال می کند:

  user
    .save({
      web3address: address,
      signedMessage: signed.signature,
    })
    .then(() => console.log("Saved"));

وقتی ما web3address مقدار در یک کاربر ذخیره می شود ، ما باید آن عملیات را رهگیری کنیم ، امضا را تأیید کنیم فقط اگر این کاربر صرفه جویی را انجام دهد ، نه یک مدیر، و اگر مشکلی نیست ذخیره کنید ، یا در غیر این صورت رد کنید (ترجیحاً با پیام خطا).

بیایید اصلاح کنیم handle عملکرد در SaveUserWeb3Address.php:

if (isset($attributes['web3address'])) {
    if (!$isSelf) {
        $actor->assertPermission($canEdit);
    }

    chdir(__DIR__ . "/../../js");
    $command = "node src/forum/scripts/verify.js "Extreme ownership" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    exec($command, $out, $err);

    if ($err) {
        return false;
    }
    $user->web3address = $attributes['web3address'];
    $user->save();
}

خطوط 6 تا 12 را اضافه کردیم: ما دایرکتوری را به فهرست حاوی اسکریپت تأیید خود تغییر می دهیم. سپس ، ما با خط فراخوانی فراخوانی خط فرمان به اسکریپت ، و در آخر اگر کد خطا باشد ، ترکیب می کنیم $err چیز دیگری غیر از دروغ (خواهد بود 0 اگر همه چیز خوب پیش رفته باشد ، ما روند پس انداز را متوقف می کنیم.

اگرچه این به مدیران اجازه نمی دهد مقدار را به دلخواه تغییر دهند ، بنابراین اجازه دهید این را اضافه کنیم. طبق هر اسناد، یک $actor دارد isAdmin یاور. نسخه نهایی ما handle روش اکنون است:

public function handle(Saving $event)
{
    $user = $event->user;
    $data = $event->data;
    $actor = $event->actor;

    $isSelf = $actor->id === $user->id;
    $canEdit = $actor->can('edit', $user);
    $attributes = Arr::get($data, 'attributes', []);

    if (isset($attributes['web3address'])) {
        if (!$isSelf) {
            $actor->assertPermission($canEdit);
        }

        if (!$actor->isAdmin()) {
            chdir(__DIR__ . "/../../js");
            $command = "node src/forum/scripts/verify.js "Extreme ownership" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
            exec($command, $out, $err);

            if ($err) {
                return false;
            }
        }
        $user->web3address = $attributes['web3address'];
        $user->save();
    }
}

وضوح خطا

آخرین کاری که باید انجام دهیم این است که در صورت عدم موفقیت در تأیید آدرس ، خطایی مناسب تر برای UX ایجاد کنیم. آ return false خیلی مفید نیست UI به سادگی هیچ کاری انجام نمی دهد. از آنجا که این یک خطای اعتبارسنجی است (ما در تأیید مالکیت کاربر برای این آدرس موفق نشده ایم) ، می توانیم a را پرتاب کنیم ValidationException:

if ($err) {
    throw new FlarumFoundationValidationException(["Signature could not be verified."]);
}

اگر تأیید صحت ما ناموفق باشد ، این را در یک پیام خطای مفید مشاهده خواهیم کرد:

پیغام خطا

قبل از استفاده از تذکر

از آنجا که در حالت توسعه هستیم ، برنامه افزودنی ما به Node و Yarn دسترسی دارد و می تواند وابستگی های Polkadot مورد نیاز برای انجام رمزنگاری را نصب کند. با این حال ، در یک محیط تولید هیچ راهی آسان برای اجرای خودکار وجود ندارد yarn install در یک بسته نصب شده توسط آهنگساز ، بنابراین اسکریپت تأیید ما بدون دخالت قابل توجه کاربر کار نمی کند. ما باید بسته verify.js اسکریپت به یک فایل است که به طور مستقیم توسط NodeJS بدون مدیر بسته قابل اجرا است. این هنوز به این معنی است که سرور تولید ما باید NodeJS را نصب کند ، اما این تنها چیزی است که لازم دارد – حداقل تا زمانی که عملکرد رمزنگاری مورد استفاده ما با عطر و طعم PHP نیز ظاهر شود.

برای جمع کردن اسکریپت خود ، در داخل پوشه JS پسوند می توانیم اجرا کنیم:

npx browserify src/forum/scripts/verify.js > dist/verify.js

این اجرا خواهد شد مرور مرور کنید بدون نصب آن ، تمام وابستگی ها را بسته بندی کرده و یک حبه JS واحد خارج کنید که در آن ذخیره می کنیم dist/verify.js. اکنون می توانیم این پرونده را به repo پسوند متصل کرده و در صورت وجود آن را هدف قرار دهیم. در واقع ، ما می توانیم برنامه افزودنی خود را تشخیص دهیم كه این انجمن در آن حضور دارد یا خیر debug منبع و فایل dist را بر اساس آن پرچم هدف قرار دهید:

if (!$actor->isAdmin()) {
    chdir(__DIR__ . "/../../js");
    if (app(FlarumFoundationConfig::class)->inDebugMode()) {
        $command = "node src/forum/scripts/verify.js "Extreme ownership" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    } else {
        $command = "node dist/verify.js "Extreme ownership" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    }
    exec($command, $out, $err);

    if ($err) {
        throw new ValidationException(["Signature could not be verified."]);
    }
}

شنونده ما نسخه سورس را اگر بخواند inDebugMode درست برگردد ، یا dist/verify.js در غیر این صورت.

نتیجه

کاربران انجمن ما اکنون می توانند آدرس های Web3 خود را به نمایه خود اضافه کنند. می توانید پسوند منتشر شده را در swader / web3address.

از آنجا که برخی از کاربران ممکن است از Chrome یا Firefox استفاده نکنند و پسوند در دسترس را نداشته باشند ، و زیرا برخی ممکن است روش های جایگزین تولید حساب را به صورت مستند ترجیح دهند اینجا، ما به مدیران اجازه می دهیم تا آدرس حسابهای کاربری خاص را به صورت دستی وارد کنند ، مشروط بر اینکه این کاربران مالکیت آدرس خود را ثابت کنند. از آنجا که این یک فرایند دستی است ، روند اثبات خارج از محدوده این آموزش است.

این اساس استفاده بعدی از این آدرس ها را پایه ریزی می کند. آنها برای پرداخت امتیازات مشارکت – رمزهای رمز – برای فعالیت کاربر در انجمن ، تشویق به بحث و گفتگوهای پر جنب و جوش و مشارکت با کیفیت استفاده می شود. ما در مورد راهنمای بعدی در مورد انجام این کار خواهیم دید.

بازخوردی در مورد این پست دارید؟ آیا نیاز به توضیح چیزی دارید؟ در صورت تمایل به با نویسنده تماس بگیرید.