• Можно ли обойти лимит запросов на BSC and ETH?

    EvgenyMamonov
    @EvgenyMamonov
    Senior software developer, system architect
    Если вы делаете запросы без ключа доступа к API - достаточно установить на сервере дополнительные IP адреса и делать запросы к API с разных IP.

    Еще, как вариант, можно отправлять запросы через прокси.
    Но вариант с IP лучше, т.к. прокси часто тормозят, глючат и т.д. + с них может еще кто-то, кроме вас, отправлять запросы. Ну и по цене значимой разницы не будет.
    Этот вариант можно использовать только если докупить доп. IP на сервер не возможно.
    Но тогда лучше просто поменять сервер/хостера ))

    Если же делаете запросы с "ключём доступа" к API - тогда придётся сделать несколько аккаунтов чтобы получить несколько таких ключей доступа, т.к. в этом случае отслеживаются запросы именно по ключу.
    Но, для надёжности, лучше взять еще и отдельный IP под каждый такой ключ чтобы на стороне BSC/ETH вас было труднее вычислить.

    Для Node.js будет что-то типа такого
    var http = require('http');
    
    var options = {
      hostname: 'www.example.com',
      localAddress: '202.1.1.1', // сюда нужно будет по очереди подставлять IP, которые у вас будут
    };
    
    var req = http.request(options, function(res) {
      res.on('data', function (chunk) {
        console.log(chunk.toString());
      });
    });


    UPD: если у вас запросы однотипные, например проверка баланса 1000 пользователей, перевод токенов сразу на 100 кошельков - есть вариант сделать отдельный смарт-контракт типа "multicall", т.е. вы делаете смарт-контракт, который в параметре принимает список кошельков для проверки балансов, в цикле делает запросы и отдаёт ответ с данными балансов всех запрошенных кошельков. По такому принципу работают "multisend" контракты, т.е. когда вы за одну транзакцию делаете сразу несколько переводов на разные кошельки.
    Ответ написан
    Комментировать
  • Что выбрать для телеграм бота? Вебхуки или лонгполлинг?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    "Long polling" периодически опрашивает телеграм сервер, за счёт этого пользователю будет казаться, что бот "торзмозит", т.к. между отправкой сообщения от пользователя и до получения ответа будут паузы.
    По крайней мере у меня было именно так.
    Еще стоит сказать о том, что "Long polling" не подойдёт если говорить о нагрузках.

    После перехода на WebHook'и - сразу заработало всё очень резво.
    Также с использованием WebHook'ов у вас будет возможность горизонтально масштабировать бота в случае роста нагрузок.
    Если бы у меня стояла такая задача - я бы использовал WebHook'и.
    Ответ написан
    3 комментария
  • Dapp разработка с чего начать?

    EvgenyMamonov
    @EvgenyMamonov
    Senior software developer, system architect
    Для разработки Dapps нужно, как минимум, разобраться как делается фронтенд - HTML/CSS/JavaScript + научиться работать фреймворками типа React/Vue.js и т.д.

    Вот отличный roadmap фронтенд разработчика https://roadmap.sh/frontend

    Далее, нужно разобраться с предметной областью, начать можно с этого roadmap https://github.com/OffcierCia/DeFi-Developer-Road-Map

    Хорошая книга Имран Башир: Блокчейн: архитектура, криптовалюты, инструменты разработки, смарт-контракты

    Далее, лучше изучить Solidity, т.к. начинать писать смарт-контракты на C/C++ будет многократно сложнее и дольше.

    Очень советую книги:
    Фролов - Создание смарт-контрактов SOLIDITY для блокчейна
    Бурков - Ethereum работа с сетью (немного устарела, но зато там много реальных примеров)
    После этих книг у меня сложилась чёткая картина

    Ну и, полезные ссылочки на официальные сайты
    https://ethdocs.org/
    https://docs.ethers.io/
    https://docs.soliditylang.org/
    Ответ написан
    Комментировать
  • Как в gorilla mux сделать серверную мидлвару?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Есть вариант использовать httptest.ResponseRecorder, я его использовал ранее для написания тестов.

    https://pkg.go.dev/net/http/httptest#ResponseRecorder

    Пример будет примерно таким
    func (m *LoggingMiddleware) LogBody(next http.Handler) http.Handler {
        fn := func(w http.ResponseWriter, r *http.Request) {
            wRecorder := httptest.NewRecorder()
            next.ServeHTTP(wRecorder, r)
            resp := wRecorder.Result()
            body, _ := io.ReadAll(resp.Body)
            // не забыть записать ответ в w
        }
    
        return http.HandlerFunc(fn)
    }


    Пример его использования из официальной документации https://pkg.go.dev/net/http/httptest#example-Respo...

    Или, можно сделать свой responseWriter, в handler передавать свой writer, а потом, после обработки, записать полученный ответ сервера в оригинальный responseWriter
    Что-то типа такого
    func (m *LoggingMiddleware) LogBody(next http.Handler) http.Handler {
        fn := func(w http.ResponseWriter, r *http.Request) {
            var buf bytes.Buffer
            logWriter := NewLogToConsoleResponseWriter(&b)
            next.ServeHTTP(logWriter, r)
            // тут у вас в buf будет ответ сервера, с которым можно работать
            // главное потом не забыть записать его в `w`
        }
    
        return http.HandlerFunc(fn)
    }
    Ответ написан
    5 комментариев
  • Как распарсить массивы из yaml в golang?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    У вас поля в структуре не экспортируемые, по этому и не работает.
    Вот рабочий пример
    package main
    
    import (
        "fmt"
        "io/ioutil"
    
        "gopkg.in/yaml.v3"
    )
    
    func main() {
    
        fmt.Println("start")
    
        c, err := readYaml("./tets.yml")
    
        if err != nil {
            panic(err.Error())
        }
    
        fmt.Println(c.Master, c.Kibana, c.Data, c.Pass, c.User)
    }
    
    type ClusterEnv struct {
        Master []string `yaml:"master,flow"`
        Data   []string `yaml:"data,flow"`
        Kibana []string `yaml:"kibana,flow"`
        User   string   `yaml:"user"`
        Pass   string   `yaml:"pass"`
    }
    
    func readYaml(filename string) (*ClusterEnv, error) {
    
        buf, err := ioutil.ReadFile(filename)
        if err != nil {
            return nil, err
        }
    
        c := &ClusterEnv{}
        err = yaml.Unmarshal(buf, c)
        if err != nil {
            return nil, fmt.Errorf("in file %q: %v", filename, err)
        }
    
        return c, nil
    }
    Ответ написан
    Комментировать
  • Нужен ли Nginx для веб приложения на Golang?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Не нужен, в Go есть полноценный веб сервер.

    Nginx есть смысл использовать в случае если есть необходимость обслуживать больше одного домена на одном и том же IP:Port, ну и для раздачи статики (изображений, CSS, Javascript и т.д.)

    Еще есть смысл использовать Nginx когда у вас большая нагрузка и ваш сервис на Go работает на нескольких серверах - Nginx'ом можно балансировать нагрузку между этими серверами.
    Ответ написан
    9 комментариев
  • Как правильно передать множество параметров в функцию в Golang?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    В таких случаях лучше передавать структуру как параметр.
    Пример
    type User struct {
        Name string
        Email string
        // все нужные поля далее
    }
    
    func CreateUser(r.Context(), user)
    Ответ написан
    Комментировать
  • Как настроить subroute в gorilla mux golang?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Надо убрать '/' из example.com/, т.е. должно быть router.Host("example.com").Subrouter()
    Ответ написан
    1 комментарий
  • Где лучше хранить глобальные структуры?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Если брать стандартную структуру проекта - я бы отталкивался от того есть ли смысл эту структуру делать доступной. Если да - тогда смотрел бы в сторону /pkg, если нет /internal.

    По хорошему, я бы сделал отдельный пакет для этой структуры и в зависимости от того используется ли она для внутренних целей или нет - положил бы пакет в /pkg или в /internal

    Для чего используется ваша глобальная структура?
    Ответ написан
    Комментировать
  • Как разрешить вызов api контроллера, только одному серверу по ip (который отправляет вебхук)?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Чаще всего, в таких случаях, используют секретный ключ, который передают или в параметрах запроса (так лучше не делать, т.к. секретный ключ будет виден в логах промежуточных прокси и сервера) или в HTTP заголовке (этот вариант безопаснее, чем передача в параметрах запроса).

    Реже используют вариант с цифровой подписью запроса. Этот вариант ощутимо безопаснее, чем передавать секретный ключ в открытом виде. При таком варианте вы не только знаете о том, что запрос отправил нужный вам сервер, но и о том, что запрос не был изменён никем (например прокси сервером).

    Но оба эти варианта требуют доработки со стороны отправляющего запрос сервера.
    Как я понимаю для вас этот вариант не доступен.

    В принципе вариант защиты по IP адресу вполне нормальный.
    Самый простой способ - настроить firewall, т.е. закрыть доступ всем к вашему сервису и открыть только для IP сервера, который делает запросы.

    Если вариант с firewall не подходит - можно обеспечивать защиту на стороне Go.
    IP можно получать из запроса HTTP https://pkg.go.dev/net/http#Request

    Вот готовый пример
    package main
    
    import (
        "fmt"
        "log"
        "net/http"
    )
    
    func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Your IP is %s", r.RemoteAddr)
    }
    
    func main() {
        http.HandleFunc("/", handler)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }

    Иногда (зависит от настроек веб сервера) реальный IP может приходить в HTTP заголовках запроса
    ip := r.Header.Get("X-Forwarded-For")
    // или
    ip := r.Header.Get("X-Real-IP")
    Ответ написан
    3 комментария
  • Куда и как добавить асинхронность в телеграм бота?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Можно сделать работу бота через webhook'и, тогда каждый запрос будет изначально обрабатываться в отдельной горутине.
    Плюс при таком подходе можно горизонтально масштабировать нагрузку, т.е. запустить бот на нескольких серверах и через какой нибудь балансировщик раскидывать на них запросы.
    Ответ написан
  • Как сделать, чтобы в редактируемом циклом значении сохранялись результаты итераций?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    попробуйте такой вариант
    var fileWork = ioutil.ReadFile("testFile.txt")
    // создаём переменную которую будем перезаписывать, сразу наполняем данными и сразу приводим к string чтобы было удобно работать
    new_fileWork := string(fileWork)
    for _, link := range arrLinks {        
        var regul = link + ".txt"
        myRegexp, err := regexp.Compile(regul)
        // new_fileWork передаем как параметр, его же и обновляем, за счёт этого при каждой итерации данные не теряются как в вашем примере
        new_fileWork = myRegexp.ReplaceAllString(new_fileWork , link)   // Редактируем.
    }
    var new_fileWorkB = []byte(new_fileWork)

    Если не поможет - сделайте песочницу чтобы можно было посмотреть полный код и напишите какой результат ожидаете получить.
    Ответ написан
  • Когда использование Cgo оправданно?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Основной недостаток использования Cgo - это снижение производительности.
    Вызовы C/C++ достаточно затратны по ресурсам, т.к. C ничего не знает о данных в Go и для вызова C необходимо полностью сохранять все регистры и переключать стек, за счёт этого и возрастают накладные расходы, соответственно снижается производительность.

    Использование Cgo имеет смысл, когда есть объёмные библиотеки написанные на C/C++, которые можно использовать. При этом написание кода на чистом Go намного затратнее, чем использование этих библиотек с Cgo.

    > в каких кейсах следует использовать cgo для улучшения производительности
    На сколько я понимаю при вызове простых функций производительность не улучшится, а наоборот, скорее ухудшится.
    Но не исключаю, что есть кейсы, когда есть серьёзные расчёты/жёсткое управление памятью (частые выделения/освобождения), когда за счёт того, что в этом случае не будет использован сборщик мусора можно получить увеличения производительности.

    У меня был подобный кейс на Perl, но принцип тот же.
    При скачивании HTML страниц размер занимаемой RAM скриптом постоянно увеличивался и в итоге "съедал" всю память на сервере.

    Задача скрипта была скачивать HTML страницы, извлекать из них все ссылки на внешние ресурсы.
    Я принял решение и написал функцию на С, которая выкачивала страницу, извлекала ссылки, очищала память и возвращала в Perl уже готовый список ссылок. Скрипты перестали постоянно "пухнуть", их можно было запустить в несколько раз больше по количеству на том же сервере + производительность стала явно выше.

    В общем всё сильно зависит от задачи, но, думаю, более 90% кейсов будет связано с тем, что намного дешевле использовать готовую библиотеку C/C++ с Cgo, чем переписать эту библиотеку на чистом Go.
    Ответ написан
    Комментировать
  • Почему при отправке письма выдает ошибку?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Подозреваю, что ошибка в этой строке, у вас указывается порт (:25), надо его убрать.
    // у вас host = "https://smtp.mail.ru:25", а должен быть "smtp.mail.ru"
    auth := smtp.PlainAuth("", from, password, host)


    На сколько я вижу вы разбираете пример из официального пакета https://pkg.go.dev/net/smtp
    Добавил туда пару комментариев
    import (
    	"log"
    	"net/smtp"
    )
    
    func main() {
    	// Set up authentication information.
            // тут порт не указан, скорее всего ругается именно в этой строке
    	auth := smtp.PlainAuth("", "user@example.com", "password", "mail.example.com")
    
    	// Connect to the server, authenticate, set the sender and recipient,
    	// and send the email all in one step.
    	to := []string{"recipient@example.net"}
    	msg := []byte("To: recipient@example.net\r\n" +
    		"Subject: discount Gophers!\r\n" +
    		"\r\n" +
    		"This is the email body.\r\n")
            // тут порт указан, т.е. host:port, но не указывается протокол типа https, тут протокол SMTP
    	err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    Ответ написан
  • Доступ к определенному значению данной библиотеки?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Вот так можно добраться
    package main
    
    import (
        "fmt"
    
        php "github.com/kovetskiy/go-php-serialize"
    )
    
    func main() {
        s := `a:6:{s:7:"__flash";a:1:{s:3:"mes";s:5:"Hello";}s:4:"__id";s:36:"3c5035d9-aea1-4f08-8325-9e598921e2a9";}`
        val, err := php.Decode(s)
        if err != nil {
            fmt.Println(err)
            return
        }
    
        session := val.(map[interface {}]interface {})
        flash := session["__flash"].(map[interface {}]interface{})
    
        fmt.Println("id", session["__id"])
        fmt.Println("mes", flash["mes"])
    }

    По хорошему надо проверять прошло ли нормально приведение типов, чтобы небыло panic
    session, ok := val.(map[interface {}]interface {})
        if !ok {
            // с val явно что-то не так, оно не соответствует типу map[interface {}]interface {}
        }
    
        flash, ok := session["__flash"].(map[interface {}]interface{})
        if !ok {
            // ...
        }
    Ответ написан
    Комментировать
  • Как получить список новых смарт конрактов?

    EvgenyMamonov
    @EvgenyMamonov
    Senior software developer, system architect
    Я с такой задачей еще не сталкивался, но вижу, что пока вообще никто не ответил.
    Постараюсь помочь.

    С уверенностью можно сказать то, что "копать" нужно в сторону анализа транзакций.

    Транзакция в результате которой был создан смарт-контракт выглядит вот так
    https://ropsten.etherscan.io/tx/0x5aa752d86932b36e...

    Обратите внимание на поле "To"
    614f96bae07ca718175271.png

    Не исключаю, что если правильно декодировать поле "Input Data" - также можно получить информацию о том, что в этой транзакции должен создаться смарт-контракт.
    614f9716b7e63008558709.png
    Т.е. определить, что в результате этой транзакции создаётся смарт-контракт и после этого взять его адрес из поля To.

    Знаю три варианта, которые можно рассмотреть для поиска решения:

    1) https://docs.etherscan.io/, это бесплатное API от Ethereum (то, что на скриншотах).

    2) https://infura.io, там, при помощи библиотек от эфира github.com/ethereum/go-ethereum я вычитывал транзакции и декодировал input data. Также там можно делать подписки.

    3) можно попробовать поставить ноду эфира к себе на сервер и библиотеками эфира подключаться уже к ней, вместо infura.io и анализировать логи. Этот вариант имеет смысл только если вам не подходит ни etherscan.io, ни infura.io

    Надеюсь это вам поможет.
    Ответ написан
    Комментировать
  • Как парсить данные с блокчейна?

    EvgenyMamonov
    @EvgenyMamonov
    Senior software developer, system architect
    Смотря какая именно вам нужна информация.

    Если брать coinmarketcap - сначала нужно сформировать список монет.
    Его можно взять из API бирж + один из способов формирования списка монет - добавление их вручную, через заявку https://support.coinmarketcap.com/hc/en-us/article....

    Если нужно узнать цены монет, объём торгов - нужно делать запросы к API бирж, на которых есть эти монеты (они бесплатные с разумными лимитами).

    Если нужно узнать эмиссию - тут тоже много вариантов, например у токенов стандарта ERC20 на блокчейне эфира - можно вызывать метод контракта totalSupply.

    Если нужны какие то данные о движениях внутри блокчейнов - тогда нужно анализировать логи транзакций каждого конкретного блокчейна.
    При этом, если не пользоваться чужими API - придётся ставить ноды нужных блокчейнов к себе на сервер и ихними же библиотеками их анализировать.
    Ответ написан
    2 комментария
  • Как сделать нормальное выключение Websocket сервера?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Если я правильно понял задачу - вам нужно корректно закрыть все соединения и завершить работу WebSocket сервера.

    По сути аккуратное закрытие клиентского WebSocket соединения выглядит так:
    - сервер отправляет в сокет frame с типом Close
    - клиент при получении frame с типом Close формирует ответ с типом Close
    - сервер ждёт ответ от клиента с типом Close (клиент подтверждает закрытие соединения)
    - сервер со своей стороны закрываете сокет, клиент со своей стороны закрывает сокет

    Т.е. вам просто нужно в цикле во все открытые сокеты отправить frame'ы с типом Close, дождаться ответа с типом Close, после чего закрыть сокет, или по таймауту закрыть сокет.

    Для работы с websocket'ами я использовал вот этот пакет https://pkg.go.dev/github.com/gobwas/ws

    Пример отправки frame'а с типом Close
    closeFrame := ws.NewCloseFrame([]byte{})
    // отправляем 
    err := ws.WriteFrame(wsconn, closeFrame)


    Пример чтения ответа от клиента
    header, err := ws.ReadHeader(wsconn)
    if header.OpCode == ws.OpClose {
        // клиент подтвердил закрытие, соединение можно закрывать
        wsconn.Close()
    }
    Ответ написан
    3 комментария
  • С чего и как начать углубляться в сферу разработки под блокчейн и криптовалюты?

    EvgenyMamonov
    @EvgenyMamonov
    Senior software developer, system architect
    > решил посмотреть в сторону разработки сервисов для криптовалют и стартапов на базе блокчейна
    Как я понимаю речь идёт о создании децентрализованных приложений.

    Сначала нужно разобраться с предметной областью, начать можно с этого roadmap https://github.com/OffcierCia/DeFi-Developer-Road-Map

    Хорошая книга Имран Башир: Блокчейн: архитектура, криптовалюты, инструменты разработки, смарт-контракты

    Как минимум для создания децентрализованных приложений, нужно разбираться в разработке смарт-контрактов под нужные вам блокчейны. Советую начать с блокчейна эфира.

    Лучше начать изучение с этой книги:
    Фролов - Создание смарт-контрактов SOLIDITY для блокчейна
    После её прочтения у вас начнёт складываться картина.

    Потом, я бы посоветовал прочесть Бурков - Ethereum работа с сетью, там много примеров смарт-контрактов, реально добавит понимания.
    Правда эти две книги уже устарели, синтаксис сейчас уже немного отличается, но альтернативы пока не знаю.

    Ну и, прочитать полностью все материалы на этих сайтах
    https://ethdocs.org/
    https://docs.soliditylang.org/
    https://web3js.readthedocs.io/

    А дальше, уже в зависимости от того, как будет работать ваше приложение прокачивать нужные направления.

    Если приложение будет работать в броузере - нужно будет прокачать front-end разработку.
    Вот хороший roadmap по frontend разработке https://roadmap.sh/frontend

    Если это будет мобильное приложение - значит мобильную разработку.
    Для мобильной разработки рекомендую использовать Flutter, на нём можно сделать приложение сразу и под iOS и под Android, но при этом оно будет нормально и быстро работать.

    Надо отметить, что разработка смарт-контрактов, front-end, mobile - это три разных специальности.
    Обычно front-end и мобильный разработчик - это разные люди, которые участвуют в разработке проекта.

    Также надо сказать, что смарт-контракты делаются достаточно быстро (ERC-20 токен можно сделать и опубликовать за несколько минут). В реальности, конечно, всё сильно зависит от ваших задач, но в целом объём работы не соизмерим с front-end/mobile.

    Скорее всего зарабатывать стабильные деньги разработкой только смарт контрактов вряд ли получится.
    Ответ написан
  • Как понять когда ставить указатель?

    EvgenyMamonov
    @EvgenyMamonov Куратор тега Go
    Senior software developer, system architect
    Указатель, по сути, хранит адрес каких то данных (переменной, структуры, слайса и т.д.).
    Иными словами он "указывает" на область данных.

    Чтобы понять как им пользоваться - сначала нужно понять для чего он вообще используется.
    Представьте что у вас есть структура, которая содержит много разных полей, в которых содержится большой объём данных.

    Например:
    type BigStruct struct {
        field1 int
        filed2 string
        field3 uint
        field4 []byte
        ...
        field50 string
    }

    Предположим, что после создания этой структуры и заполнения всех её полей она занимает в памяти 300мб.

    Если вы сделаете функцию, которая будет принимать такую структуру как агрумент, например вот так
    func Report(s BigStruct)
    то при каждом вызове этой функции вся структура (300мб) каждый раз будут копироваться.
    Пример:
    s := BigStruct{}
    // заполняем поля
    Report(s)


    Чтобы избежать такой мега нагрузки - можно передавать не копию данных, а указатель, т.е. адрес в памяти, где хранится сама структура.
    Для этого нужно объявить агрумент функции как указатель, т.е. ставим *.
    func Report(s *BigStruct)
    А код уже будет выглядеть вот так.
    s := BigStruct{}
    // заполняем поля
    Report(&s) // тут добавился & - берём адрес структуры, а не саму структуру

    Или второй вариант
    // создаём переменную s сразу с типом указатель на BigStruct
    s := &BigStruct{}
    // заполняем поля
    Report(s) // поскольку s уже является указателем - & тут не нужен


    В общем * используется:
    - когда нужно объявить переменную
    var s *BigStruct
    - когда нужно прочитать/записать значение, которое храниться по адресу указателя
    var i *int
        i = new(int)
        *i = 10 // пишем значение
    
        fmt.Printf("i: %v\n", i)
        fmt.Printf("*i: %v\n", *i)

    Вывод будет примерно таким
    i: 0xc0000160d8 (это адрес памяти, где лежит значение переменной i)
    *i: 10 (а это её значение)


    & (амперсанд) используется когда нужно получить адрес переменный.

    Еще один вариант применения - если нужно иметь возможность модифицировать данные у параметра функции. Если нужны примеры - дайте знать, я напишу.
    Ответ написан
    12 комментариев