برنامه نویسی

ساخت تأیید کننده ایمیل با Go

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

بگویید، اگر می خواهید به من ایمیل بزنید، ترکیب های ممکن می تواند باشد aniket.pal@companymail.com، pal.aniket@companymail.com، paniket@companymail.com، aniketpal@comapanymail.com و مرتبط ایجاد یک لیست عظیم از جایگشت و ترکیب های متعدد. برای کوتاه کردن فضای جستجو، یک برنامه کاربردی ایجاد خواهیم کرد تا بررسی کنیم که آیا companymail.com معتبر است یا نه.

هدف از آموزش این است که به شما درک کاملی از قابلیت های go-lang بدهد. ما از هیچ ماژول شخص ثالثی استفاده نخواهیم کرد، فقط با ماژول های اصلی Go که خواهیم ساخت.

چرا Go را انتخاب کنید؟ 🤨

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

هر سازمانی که به مقیاس نگاه می کند، از Golang برای ساخت ریزسرویس های کانتینری استفاده می کند.

علاوه بر این، Go یک زبان عالی برای ایجاد وب سرورها و خدمات وب ساده و در عین حال کارآمد است. این یک بسته HTTP داخلی را ارائه می دهد که شامل ابزارهایی برای ایجاد سریع وب یا سرور فایل است.

همچنین، اگر به اکوسیستم Cloud Native علاقه دارید، Go زبانی است که باید با آن شروع کنید. در این مورد، شما هرگز یک سرور باطن را با Go checkout ایجاد نکرده اید.

چه چیزی خواهیم ساخت؟ 👨‍🚒

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

ساخت Backend 🚪

ما از قسمت جلویی برنامه برای دریافت دامنه مورد نیاز خود استفاده می کنیم.

تعریف پکیج اصلی و وارد کردن بسته های مورد نیاز.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"

    "github.com/gorilla/mux"
)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر در نحوه نصب سردرگم هستید gorilla/mux و این بسته چه کاری انجام می دهد، ساخت سرور با Go زیر 10 دقیقه را بخوانید. از هم‌اکنون، کمی دیرتر APIهای REST را می‌سازیم. اول، اجازه دهید فرض کنیم که دریافت می کنیم domain به عنوان رشته ای که باید روی آن کار کنیم.

تعریف یک کنترل کننده، isValidDomain. کنترل کننده فقط یک پارامتر را می گیرد و می گوید:domain از نوع رشته

func isValidDomain(domain string){
// controller code goes here
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید کار را با عملکرد کنترلر شروع کنیم. ابتدا، تعریف متغیرهایی برای بررسی اینکه آیا دامنه خاص دارای رکوردهای MX، رکوردهای SPF و رکوردهای DMARC است یا خیر. اگر دارند چه سوابقی دارند.

  var hasMX, hasSPF, hasDMARC bool 
  var spfRecord string
  var dmarcRecord  string 
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون زمان آن است که داده ها را در اینترنت بررسی کنید. بسته های فشرده داخلی Go، کار را انجام دهید net/http بسته بندی در این صورت، شما باید از ruby ​​یا nodej هایی استفاده می کردید که برای نصب باینری های بیشتر به آن نیاز داشتید.

    mxRecords,err := net.LookupMX(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    if len(mxRecords)>0{
        hasMX = true 
    }
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما از بسته نت برای دریافت آن استفاده می کنیم mxRecords، در صورتی که داده ها را دریافت نکنیم، خطا دریافت می کنیم. خطا را در خطوط زیر بررسی می کنیم. برای بررسی اینکه آیا mxRecords را دریافت کرده‌ایم، بررسی می‌کنیم که آیا طول آرایه بیش از یک است یا خیر، به این معنی که آیا آرایه حاوی بیش از یک عنصر است.

    txtRecords, err := net.LookupTXT(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range txtRecords{
        if strings.HasPrefix(record,"v=spf1"){
            hasSPF = true 
            spfRecord = record
            break
        }
    }
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مشابه mxRecords، ما با استفاده از داده ها و خطاها را مدیریت می کنیم := اپراتور.

یاد آوردن، := یک اعلامیه است، در حالی که = یک اپراتور انتساب است.

پس از رسیدگی به خطا، از آن عبور می کنیم txtRecords slice، از آنجایی که ما به ایندکس نیاز نداریم، آن را نادیده می گیریم _. از آنجایی که Golang اجازه داشتن متغیرهای استفاده نشده را نمی دهد، از عملگر زیر خط استفاده می کنیم. هنگامی که ما حلقه بیش از txtRecords، بررسی می کنیم که آیا نسخه رکورد زیر وجود دارد یا خیر spf1. اگر چنین است، علامت گذاری می کنیم hasSPF مثبت و ذخیره مقدار رکورد در spfRecord.

    dmarcRecords, err := net.LookupTXT("_dmarc." + domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range dmarcRecords{
        if strings.HasPrefix(record ,"v=DMARC1"){
            hasDMARC = true
            dmarcRecord = record 
            break
        }
    }
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به دنبال dmarcRecords تقریبا شبیه به txtRecords. اضافه می کنیم _dmarc. در پیشوند url دامنه، و از net بسته ای برای بررسی اینکه آیا رکوردهای مربوطه برای dmarc وجود دارد یا خیر. هنگام عبور از برش dmarcRecords، بررسی می کنیم که آیا نسخه رکورد وجود دارد یا خیر DMARC1 hasDMARC true را علامت گذاری می کنیم و مقدار رکورد را در dmarcRecord ذخیره می کنیم.

برای بررسی اینکه آیا ما مقادیر و کنترل کننده خود را دریافت می کنیم isValidDomain خوب کار می کند بیایید مقادیری را که تا کنون داشته ایم چاپ کنیم.

fmt.Printf("domain=%v\n,hasMX=%v\n,hasSPF=%v\n,spfRecord=%v\n,hasDMARC=%v\n,dmarcRecord=%v\n",domain,hasMX,hasSPF,spfRecord,hasDMARC,dmarcRecord)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تدوین isValidDomain اسکریپت، ما داریم.

func isValidDomain(domain string){
    var hasMX, hasSPF, hasDMARC bool 
    var spfRecord string
    var dmarcRecord  string 

    mxRecords,err := net.LookupMX(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    if len(mxRecords)>0{
        hasMX = true 
    }

    txtRecords, err := net.LookupTXT(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range txtRecords{
        if strings.HasPrefix(record,"v=spf1"){
            hasSPF = true 
            spfRecord = record
            break
        }
    }

    dmarcRecords, err := net.LookupTXT("_dmarc." + domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range dmarcRecords{
        if strings.HasPrefix(record ,"v=DMARC1"){
            hasDMARC = true
            dmarcRecord = record 
            break
        }
    }


    fmt.Printf("domain=%v\n,hasMX=%v\n,hasSPF=%v\n,spfRecord=%v\n,hasDMARC=%v\n,dmarcRecord=%v\n",domain,hasMX,hasSPF,spfRecord,hasDMARC,dmarcRecord)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که، عملکرد ما کار می کند. بیایید شروع به کار روی ساخت API های REST خود کنیم. در ابتدا، ما 2 ساختار را تعریف خواهیم کرد، DomainURL و DomainVar. یکی برای رمزگشایی ورودی و دیگری برای رمزگذاری خروجی.

type DomainURL struct{
    DomainURL string `string:"domainurl"`
}

type DomainVar struct{
    Domain string `json:"domain"`
    HasMX bool `json:"hasmx"`
    HasSPF bool `json:"haspf"`
    SpfRecord string `json:"spfrecord"`
    HasDMARC bool `json:"hasdmarc"`
    DmarcRecord string `json:"dmarcRecord"`
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر شما یک توسعه دهنده جاوا اسکریپت هستید ساختار مشابه کلاس ES6 است. حال اجازه دهید یک برش را تعریف کنیم که شبیه بردارها در C++ است.

var domainVars []DomainVar
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

ابتدا یک روتر درخواست جدید ایجاد کنید. روتر روتر اصلی برنامه وب ما است و بعداً به عنوان پارامتر به سرور ارسال می شود. تمام اتصالات HTTP را دریافت می کند و آن را به کنترل کننده های درخواستی که در آن ثبت می کنیم ارسال می کند. ما روتر را در تابع اصلی ایجاد می کنیم. پس از ثبت نام روتر، اجازه دهید نقاط پایانی را با استفاده از HandleFunction تعریف کنیم. ما HandleFunction را توسط r.HandleFunc(…) می نامیم.

func main(){

    r := mux.NewRouter()
    r.HandleFunc("/form",formHandler).Methods("POST")

        fmt.Print("Starting server at port 8000\n")
        log.Fatal(http.ListenAndServe(":8000",r))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در HandleFunc ما 2 پارامتر را ارائه می دهیم، اول مسیری که می خواهیم جادو را در آن ببینیم و ثانیا نام تابع کنترلر خاصی را می نویسیم که جادو را انجام می دهد. GET و POST متناظر به معنای عادی است، برای ساختن عملیات CRUD برای برنامه کافیست PUT و DELETE را با توجه به مسیر اضافه کنید.

پورت سرور را می توان به هر مقدار مورد نیاز منتقل کرد. اسکریپت برای POST Request Handler یا همان Handler یک مرحله در یک زمان.

func formHandler(w http.ResponseWriter, r *http.Request){
   w.Header().Set("Content-Type","application/json")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما هدر را برای کنترلر خاص در اینجا تعریف می کنیم. ما فقط هدر “Content-Type” را روی “application/json” تنظیم می کنیم.

func formHandler(w http.ResponseWriter, r *http.Request){
   w.Header().Set("Content-Type","application/json")

   var domainUrl DomainURL
   json.NewDecoder(r.Body).Decode(&domainUrl)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون مقدار دامنه را در تابع isValidDomain ارسال می کنیم. ما همه را در a ذخیره می کنیم domainVar. پست کنید که domainVar را به آن اضافه می کنیم domainVars تکه. سپس از بسته رمزگذاری برای رمزگذاری تمام داده های domainVars و همچنین برگرداندن آن در همان خط استفاده می کنیم. در نهایت، ارسال درخواست POST، واکشی داده ها، ذخیره سازی در یک ساختار، الحاق به برش و سپس برگرداندن برش.

func formHandler(w http.ResponseWriter, r *http.Request){
    w.Header().Set("Content-Type","application/json")

    var domainUrl DomainURL
    json.NewDecoder(r.Body).Decode(&domainUrl)

    domainVar  := isValidDomain(domainUrl.DomainURL)
    domainVars = append(domainVars, domainVar)

    json.NewEncoder(w).Encode(domainVars)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که مسیر ما پیکربندی شده است، اجازه دهید تابع isValidDomain خود را طوری تغییر دهیم که بتوانیم مقادیر مورد نیاز برای ساختار DomainVar را بدست آوریم.

func isValidDomain(domain string) DomainVar{
    var hasMX, hasSPF, hasDMARC bool 
    var spfRecord string
    var dmarcRecord  string 

    mxRecords,err := net.LookupMX(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    if len(mxRecords)>0{
        hasMX = true 
    }

    txtRecords, err := net.LookupTXT(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range txtRecords{
        if strings.HasPrefix(record,"v=spf1"){
            hasSPF = true 
            spfRecord = record
            break
        }
    }

    dmarcRecords, err := net.LookupTXT("_dmarc." + domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range dmarcRecords{
        if strings.HasPrefix(record ,"v=DMARC1"){
            hasDMARC = true
            dmarcRecord = record 
            break
        }
    }

    fmt.Printf("domain=%v\n,hasMX=%v\n,hasSPF=%v\n,spfRecord=%v\n,hasDMARC=%v\n,dmarcRecord=%v\n",domain,hasMX,hasSPF,spfRecord,hasDMARC,dmarcRecord)

    var domainVar DomainVar
    domainVar.Domain = domain
    domainVar.HasMX = hasMX
    domainVar.HasSPF = hasSPF
    domainVar.SpfRecord = spfRecord
    domainVar.HasDMARC = hasDMARC
    domainVar.DmarcRecord = dmarcRecord

    return domainVar 
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تابع اصلاح شده دارای یک نوع بازگشتی است DomainVar که در نهایت شیء از همان نوع را برمی گرداند. ما یک نمونه از نوع DomainVar ایجاد می کنیم و سپس شروع به تخصیص مقادیر برای هر کلید می کنیم. در نهایت شی را برگردانید.

*کد Backend کامل تا کنون این است: *

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"

    "github.com/gorilla/mux"
)

type DomainURL struct{
    DomainURL string `string:"domainurl"`
}

type DomainVar struct{
    Domain string `json:"domain"`
    HasMX bool `json:"hasmx"`
    HasSPF bool `json:"haspf"`
    SpfRecord string `json:"spfrecord"`
    HasDMARC bool `json:"hasdmarc"`
    DmarcRecord string `json:"dmarcRecord"`
}

var domainVars []DomainVar


func formHandler(w http.ResponseWriter, r *http.Request){
    w.Header().Set("Content-Type","application/json")

    var domainUrl DomainURL
    json.NewDecoder(r.Body).Decode(&domainUrl)

    domainVar  := isValidDomain(domainUrl.DomainURL)
    domainVars = append(domainVars, domainVar)

    json.NewEncoder(w).Encode(domainVars)
}

func isValidDomain(domain string) DomainVar{
    var hasMX, hasSPF, hasDMARC bool 
    var spfRecord string
    var dmarcRecord  string 

    mxRecords,err := net.LookupMX(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    if len(mxRecords)>0{
        hasMX = true 
    }

    txtRecords, err := net.LookupTXT(domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range txtRecords{
        if strings.HasPrefix(record,"v=spf1"){
            hasSPF = true 
            spfRecord = record
            break
        }
    }

    dmarcRecords, err := net.LookupTXT("_dmarc." + domain)

    if err != nil{
        log.Printf("Error: %v\n",err)
    }

    for _, record := range dmarcRecords{
        if strings.HasPrefix(record ,"v=DMARC1"){
            hasDMARC = true
            dmarcRecord = record 
            break
        }
    }

    fmt.Printf("domain=%v\n,hasMX=%v\n,hasSPF=%v\n,spfRecord=%v\n,hasDMARC=%v\n,dmarcRecord=%v\n",domain,hasMX,hasSPF,spfRecord,hasDMARC,dmarcRecord)

    var domainVar DomainVar
    domainVar.Domain = domain
    domainVar.HasMX = hasMX
    domainVar.HasSPF = hasSPF
    domainVar.SpfRecord = spfRecord
    domainVar.HasDMARC = hasDMARC
    domainVar.DmarcRecord = dmarcRecord

    return domainVar 
}


func main(){
    r := mux.NewRouter()

    r.HandleFunc("/form",formHandler).Methods("POST")


    fmt.Print("Starting server at port 8000\n")
    log.Fatal(http.ListenAndServe(":8000",r))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساخت Frontend 🎨

ما در واقع روی زیباسازی برنامه تمرکز نخواهیم کرد، بلکه تمرکزمان بر انجام کارها خواهد بود. برای قسمت جلویی، ما به یک بسته نیاز داریم go-fiber. اگرچه، می‌توان frontend را بدون نصب هیچ بسته اضافی توسعه داد، دلیل استفاده از go-fiber درک نحوه نصب و کار با بسته‌های خارجی است.

بیایید با ایجاد دایرکتوری و تغییر دایرکتوری کاری خود به آن شروع کنیم.

mkdir verifier-frontend && cd verifier-frontend
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

راه‌اندازی فایل main.go برای frontend

touch main.go
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با اجرای دستور، یک فایل خالی در دایرکتوری کاری ما در اینجا ایجاد می کند که verfier-frontend است.

باید برای فایل Go یک پکیج تعریف کنیم. از آنجایی که این فایل اصلی ما خواهد بود، بسته main را اضافه می کنیم.

package main
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خط بالا اولین خط برنامه است.

حالا بیایید تمام بسته های لازم برای ساخت اپلیکیشن خود را وارد کنیم 🛍️

import (
    "log"

    "github.com/gofiber/fiber/v2"
)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر من جای شما بودم و همه واردات را می خواندم، خیلی گیج می شدم. اما، به من اعتماد کنید وقتی وبلاگ را خواندید، می‌توانم شرط ببندم که ایده روشنی برای اینکه چرا بسته‌های زیر را گنجانده‌ایم، خواهید داشت.

ایجاد فایل go.mod که تمام بسته های مورد نیاز را ذخیره می کند، شبیه به package.json است.

go mod init github.com/Aniket762/namaste-go/verifier-front
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای اطمینان از اینکه فایل go.mod با کد منبع موجود در ماژول مطابقت دارد، اجرا می کنیم

go mod tidy
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تمام بسته های وارد شده به جز یک مورد، قبلاً با فایل باینری که هنگام نصب Go اجرا کرده اید، وجود دارد. بنابراین، اجازه دهید Gorilla Mux را نصب کنیم.

go get github.com/gofiber/fiber/v2
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر یک توسعه دهنده جاوا اسکریپت هستید، دستور زیر تقریباً شبیه npm install است

در حال حاضر، همه ما آماده ایم که شروع به ساخت ظاهر خود کنیم. بیایید کد را از اسناد رسمی Go Fiber دریافت کنیم، اجرا کنیم و آن را درک کنیم.

package main

import (
    "log"

    "github.com/gofiber/fiber/v2"
)

func main() {
    app := fiber.New()

    app.Get("https://dev.to/", func (c *fiber.Ctx) error {
        return c.SendString("Hello From Verifier's Frontend 👋")
    })

    log.Fatal(app.Listen(":3000"))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از وارد کردن بسته ها، تابع اصلی را می نویسیم. تابع اصلی اولین تابعی است که با کامپایل و اجرای برنامه شروع می شود. ما یک نمونه، برنامه از نوع فیبر ایجاد می کنیم. ما مختصر c را برای متن می نویسیم.

درخواست و پاسخ HTTP در زمینه نگهداری می شود که توسط ساختار Ctx نشان داده می شود. متدهایی برای بدنه درخواست، هدرهای HTTP، آرگومان ها و رشته پرس و جو ارائه می کند.

بیایید سرور را در پورت 3000 اجرا کنیم. شما می توانید بنا به راحتی پورت را تغییر دهید. حالا اجازه دهید سرور را اجرا کنیم و بررسی کنیم که آیا همه چیز هماهنگ است یا خیر. به ترمینال خود بروید و اجرا کنید go run main.go

برو فیبر پست ترمینال

شما باید این را در ترمینال خود دریافت کنید که نشان می دهد پورتی که با آن در حال اجرا هستیم، تعداد کنترل کننده ها، شناسه های پردازش و تعداد فرآیندهایی که در حال اجرا هستیم. داده‌های زیر زمانی که به سمت ساخت برنامه‌های پیچیده‌تر حرکت می‌کنید، کاربرد واقعی دارند. هدایت به localhost:3000 یا هر پورتی که نوشته اید.

به رابط کاربری فیبر بروید

دیگر 💚

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

اکنون، درست مانند هر آموزش دیگری، اجازه دهید آموزش را با یک کار به پایان برسانیم. ما با استفاده از API های REST توسعه داده ایم gorilla/mux و استارتر جلویی با go-fiber. سعی کنید مسیر POST را برای فرانت‌اند بسازید. در صورتی که قادر به ساختن نباشید، به زودی مقاله ای در مورد ساخت فرانت اند با Go-lang ارسال خواهم کرد. بهترین ها برای ساختن یک نمای کامل. اینک، وقتی من نحوه ساخت فرانت اند با استفاده از go-fiber را منتشر می کنم از دست ندهید من را در شبکه های اجتماعی من دنبال کنید ^-^

اگر برنامه را توسعه داده اید یا چیزی برای بحث در زیر نور خورشید دارید، می توانید با من در لینکدین تماس بگیرید یا توییتر 💖

اگر سازمانی را اداره می کنید و می خواهید که من فیلم های آموزشی بنویسم یا بسازم، لطفاً با من ارتباط برقرار کنید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

همچنین ببینید
بستن
دکمه بازگشت به بالا