JavaRush /Java blogi /Random-UZ /Biz maqolalar guruhiga obuna bo'lish imkoniyatini qo'shmo...

Biz maqolalar guruhiga obuna bo'lish imkoniyatini qo'shmoqdamiz. (1-qism) - "Java loyihasi A dan Z gacha"

Guruhda nashr etilgan
Salom! Bugun biz JavaRush-dagi maqolalar guruhiga obuna qo'shamiz. Bu GitHub-dagi JRTB-5 soniga mos keladi . Tushuntirishga ruxsat bering: JavaRush-da Maqolalar deb nomlangan bo'lim mavjud va unda Maqolalar guruhlari mavjud. Maqsad telegram boti orqali bir yoki bir nechta guruhlardan yangi maqola haqida bildirishnomalarni olishdir."A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-1-qism

JRTB-5 qo'shing

Aytaylik, men Muvaffaqiyat hikoyalari guruhidagi maqolalarga qiziqaman . Shuning uchun men ushbu guruhning yangilanishlariga obuna bo'lishni va har safar yangi nashrga havola olishni xohlayman. Ushbu vazifaning bir qismi sifatida biz JavaRush-da guruhlar bilan ishlash uchun ochiq API-dan qanday foydalanishni o'rganishimiz kerak. Aynan shu vaqtda shunday narsa keldi. Bu yerda ochiq API tavsifiga havola .
Do'stlar! Loyiha uchun yangi kod yoki yangi maqola qachon chiqarilganini darhol bilishni xohlaysizmi? tg kanalimga qo'shiling . U erda men o'zimning maqolalarimni, fikrlarimni va ochiq manbalarni ishlab chiqishni birga to'playman.

Swagger nima? Keling, buni hozir aniqlaylik

Biz hali janjal haqida gapirmadik. Bilmaganlar uchun men qisqacha tushuntiraman: bu siz serverning API-ni ochiq ko'rishingiz va unga ba'zi so'rovlar berishga harakat qilishingiz mumkin bo'lgan joy. Odatda, shafqatsizlik mumkin bo'lgan so'rovlarni guruhlaydi. Bizning holatlarimizda uchta guruh mavjud: forum-savol , guruh , post . Har bir guruhda ushbu so'rovni yaratish uchun barcha kerakli ma'lumotlarni ko'rsatadigan bir yoki bir nechta so'rovlar bo'ladi (ya'ni, qanday qo'shimcha parametrlarni o'tkazish mumkin, ular bilan nima qilish kerak, qanday http usuli va boshqalar). Men sizga ushbu mavzu bo'yicha ko'proq o'qishni va ko'rishni maslahat beraman, chunki bu deyarli har biringiz duch keladigan rivojlanish qismidir. Buni tushunish uchun keling, JavaRush-da nechta guruh borligini bilib olaylik. Buni amalga oshirish uchun guruh boshqaruvchisi guruhini kengaytiring va so'rovni olish /api/1.0/rest/groups/count ni tanlang . Bu bizga JavaRushdagi guruhlar sonini qaytaradi. Keling, ko'rib chiqaylik: "A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-2 qismRasmda ushbu so'rov bir nechta parametrlarni (so'rov, turdagi, filtr) qo'llab-quvvatlashini ko'rsatadi. Bu soʻrovni sinab koʻrish uchun “Sinab koʻring” tugmasini topishingiz kerak , shundan soʻng ushbu parametrlar sozlanishi mumkin: "A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-3 qismSiz u yerda tur, filtr va soʻrovni ham sozlashingiz mumkin (bu yerda aslida qiziq: bu matnli qidiruv boʻladi. guruh). Ammo hozircha, keling, uni hech qanday cheklovlarsiz ishga tushiramiz va JavaRush-da qancha guruh borligini ko'raylik. Buning uchun Execute tugmasini bosing. Quyida ushbu so'rovga javob (Server javobi bo'limida) bo'ladi: Biz jami 30 ta"A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-4 qism guruh borligini ko'ramiz , bu so'rov 95 msda bajarilgan va javobda bir nechta sarlavhalar mavjud. Keyinchalik, ba'zi parametrlarni sozlashga harakat qilaylik. COMPANY qiymatiga teng tip parametrini tanlaymiz va natija qanday o'zgarishini ko'ramiz: Ulardan 4 tasi bor.Buni qanday tekshirish mumkin? Bu oson: siz veb-saytga kirishingiz, maqola bo'limini topishingiz, barcha guruhlarni tanlashingiz va u erda tegishli filtrni qo'shishingiz mumkin ( https://javarush.com/groups/all?type=COMPANY ). Va ha, haqiqatan ham, ulardan faqat 4 tasi bor. Garchi aslida uchtasi bor bo'lsa ham :D Hozircha u mos keladi. Darvoqe, universitetlarni tekshirsak, hali yo‘q. Ko'ngil ochish uchun Javarush-ga kirgan va tizimga kirmagan brauzerda filter = MY o'rnatsangiz nima bo'lishini ko'ring. Swagger haqida ko'proq - Habré-dagi ushbu maqolada ."A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-5 qism"A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-6 qism

Guruhlar uchun Javarush API uchun mijoz yozish

Endi ochiq API asosida biz so'rovlar qila oladigan, javob oladigan va qanday ob'ektlar kelishini aniq biladigan Java mijozini yozamiz. Shuningdek, biz ob'ektlarni "Modellar" bo'limidan (sahifaning eng pastki qismida) shablondan olamiz . Keling, yangi paket yaratamiz va uni javarushclient xizmati, omborxona yonidagi deb nomlaymiz. Kelajakda biz buni Javarush hamjamiyatidagi alohida kutubxonaga ko'chiramiz va undan faqat bog'liqlik sifatida foydalanamiz. Avvalo, JavaRush API-ga http so'rovlarini yaratish uchun kutubxona bo'lgan Unitrest-ni qo'shishingiz kerak:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
Va versiyani xususiyatlar blokiga qo'ying:
<unirest.version>3.11.01</unirest.version>
Bizda qaramlik paydo bo'lgandan so'ng, kod qo'shishni boshlashimiz mumkin. Keling, JavaRushGroupClient guruhlari uchun mijoz va JavaRushGroupClientImpl sinfida dastur yarataylik. Lekin birinchi navbatda siz DTO (ma'lumotlarni uzatish ob'ektlari) - ya'ni ob'ektlari mijoz uchun zarur bo'lgan barcha ma'lumotlarni olib yuradigan sinflarni yaratishingiz kerak. Barcha modellarni shablonda ko'rish mumkin.Eng pastki qismida Modellar bo'limi mavjud bo'lib , unda siz ularni sanashingiz mumkin. GroupDiscussionInfo swaggerda shunday ko'rinadi: javarushclient to'plamida biz dto"A dan Zgacha Java loyihasi": maqolalar guruhiga obuna bo'lish imkoniyatini qo'shish.  1-7 qism paketini yaratamiz , unga swagger ma'lumotlari asosida quyidagi sinflarni qo'shamiz:
  • MeGroupInfoStatus :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Member group status.
    */
    public enum MeGroupInfoStatus {
       UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
    }

  • MeGroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * Group information related to authorized user. If there is no user - will be null.
    */
    @Data
    public class MeGroupInfo {
       private MeGroupInfoStatus status;
       private Integer userGroupId;
    }

  • GroupInfoType :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Info type;
    */
    public enum GroupInfoType {
       UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
    }

  • UserDiscussionInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * DTO for User discussion info.
    */
    @Data
    public class UserDiscussionInfo {
    
       private Boolean isBookmarked;
       private Integer lastTime;
       private Integer newCommentsCount;
    }

  • GroupVisibilitysdholati :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Keyin - GroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    import lombok.ToString;
    
    /**
    * Group Info DTO class.
    */
    @Data
    @ToString
    public class GroupInfo {
    
       private Integer id;
       private String avatarUrl;
       private String createTime;
       private String description;
       private String key;
       private Integer levelToEditor;
       private MeGroupInfo meGroupInfo;
       private String pictureUrl;
       private String title;
       private GroupInfoType type;
       private Integer userCount;
       private GroupVisibilityStatus visibilityStatus;
    }

GroupInfo va GroupDiscussionInfo deyarli bir xil bo'lgani uchun keling , ularni merosga bog'laylik - GroupDiscusionInfo :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {

   private UserDiscussionInfo userDiscussionInfo;
   private Integer commentsCount;
}
Shuningdek, bizga GroupFilter so'rovi uchun filtr kerak bo'ladi :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* Filters for group requests.
*/
public enum GroupFilter {

   UNKNOWN, MY, ALL
}
ID bo'yicha olish so'rovida u GroupDiscussionInfo-ni qaytaradi va guruhlar to'plamiga so'rovda siz ham GroupInfo, ham GroupDiscussionInfo-ni olishingiz mumkin. So'rovlar turi, so'rovi, filtri, ofset va chegarasiga ega bo'lishi mumkinligi sababli, keling, alohida GroupRequestArgs sinfini yaratamiz va uni quruvchi sinfiga aylantiramiz (quruvchi naqsh nima ekanligini o'qing):
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.*;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {

   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   /**
    * specified where to start getting groups
    */
   private final Integer offset;
   /**
    * Limited number of groups.
    */
   private final Integer limit;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if(nonNull(query)) {
           queries.put("query", query);
       }
       if(nonNull(type)) {
           queries.put("type", type);
       }
       if(nonNull(filter)) {
           queries.put("filter", filter);
       }
       if(nonNull(offset)) {
           queries.put("offset", offset);
       }
       if(nonNull(limit)) {
           queries.put("limit", limit);
       }
       return queries;
   }
}
Guruhlar sonini qidirish uchun u biroz farq qiladi. Unda faqat so'rov, tur va filtr mavjud. Va siz kodni takrorlashni istamaganga o'xshaysiz. Shu bilan birga, agar siz ularni birlashtira boshlasangiz, quruvchilar bilan ishlashda xunuk bo'lib chiqadi. Shuning uchun men ularni ajratib, kodni takrorlashga qaror qildim. GroupCountRequestArgs shunday ko'rinadi :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Builder;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if (nonNull(query)) {
           queries.put("query", query);
       }
       if (nonNull(type)) {
           queries.put("type", type);
       }
       if (nonNull(filter)) {
           queries.put("filter", filter);
       }
       return queries;
   }
}
Ha, men oxirgi ikki sinfda so'rov yaratish uchun xaritani tayyorlaydigan populateQueries usuli borligini aytmadim (keyinroq ko'rasiz). Yuqorida tavsiflangan sinflarga asoslanib, keling, JavaRushGroupClient uchun interfeys yarataylik :
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;

import java.util.List;

/**
 * Client for Javarush Open API corresponds to Groups.
 */
public interface JavaRushGroupClient {

    /**
     * Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}.
     * @return the collection of the {@link GroupInfo} objects.
     */
    List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);

    /**
     * Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}
     * @return the collection of the {@link GroupDiscussionInfo} objects.
     */
    List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);

    /**
     * Get count of groups filtered by provided {@link GroupRequestArgs}.
     *
     * @param countRequestArgs provided {@link GroupsCountRequestArgs}.
     * @return the count of the groups.
     */
    Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);

    /**
     * Get {@link GroupDiscussionInfo} by provided ID.
     *
     * @param id provided ID.
     * @return {@link GroupDiscussionInfo} object.
     */
    GroupDiscussionInfo getGroupById(Integer id);
}
Biz GroupInfo yoki GroupDiscussionInfo maʼlumotlarini olishni istagan holda ikki xil soʻrov. Aks holda, bu so'rovlar bir xil bo'ladi va yagona farq shundaki, birida includeDiscussion bayrog'i rost, ikkinchisida esa noto'g'ri bo'ladi. Shuning uchun uchta emas, 4 ta usul mavjud edi. Endi amalga oshirishni boshlaylik:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* Implementation of the {@link JavaRushGroupClient} interface.
*/
@Component
public class JavaRushGroupClientImpl implements JavaRushGroupClient {

   private final String javarushApiGroupPath;

   public JavaRushGroupClientImpl(@Value("${javarush.api.path}") String javarushApi) {
       this.javarushApiGroupPath = javarushApi + "/groups";
   }

   @Override
   public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupInfo>>() {
               })
               .getBody();
   }

   @Override
   public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupDiscussionInfo>>() {
               })
               .getBody();
   }

   @Override
   public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
       return Integer.valueOf(
               Unirest.get(String.format("%s/count", javarushApiGroupPath))
                       .queryString(countRequestArgs.populateQueries())
                       .asString()
                       .getBody()
       );
   }

   @Override
   public GroupDiscussionInfo getGroupById(Integer id) {
       return Unirest.get(String.format("%s/group%s", javarushApiGroupPath, id.toString()))
               .asObject(GroupDiscussionInfo.class)
               .getBody();
   }


}
Men allaqachon tanish Value izohidan foydalanib, konstruktorda API yo'lini qo'shaman. Bu izoh ichidagi qiymat xususiyatlar faylidagi maydonga mos kelishini bildiradi. Shuning uchun, application.properties ga yangi qator qo'shamiz:
javarush.api.path=https://javarush.com/api/1.0/rest
Bu qiymat endi barcha API mijozlari uchun bir joyda bo‘ladi va agar API yo‘li o‘zgarsa, biz uni tezda yangilaymiz. Ilgari men mikroskop bilan mixlarni bolg'aladim, Unirest orqali http so'rovidan javob oldim, uni satrga tarjima qildim va keyin Jekson orqali bu satrni tahlil qildim ... Bu qo'rqinchli, zerikarli va ko'p qo'shimcha narsalarni talab qildi. Ushbu kutubxonada siz uning qanday ko'rinishini ko'rishingiz mumkin. Uni qo'limga olishim bilan men hamma narsani qayta ko'rib chiqaman.
Ushbu kutubxonani yangilamoqchi bo'lgan har bir kishi - faqat unirest kutubxonasi vositalaridan foydalangan holda qabul qiluvchi ob'ektlarni qo'shish - shaxsiy xabarda yoki kutubxonaning o'zida yangi nashr sifatida yozing. Bu siz uchun haqiqiy ish tajribasi bo'ladi, lekin men bunga qarshi emasman. Men kodni to'liq ko'rib chiqaman va kerak bo'lsa yordam beraman.
Endi savol tug'iladi: bizning kodimiz biz kutgandek ishlaydimi? Javob oson: ular uchun testlar yozish kifoya. Bir necha marta aytganimdek, ishlab chiquvchilar testlarni yozish imkoniyatiga ega bo'lishlari kerak. Shuning uchun, Swagger UI-dan foydalanib, biz so'rovlarni yuboramiz, javoblarni ko'rib chiqamiz va kutilgan natija sifatida ularni testlarga almashtiramiz. Guruhlar soni statik emas va o'zgarishi mumkinligini darhol payqagan bo'lishingiz mumkin. Va siz haqsiz. Bitta savol - bu raqam qanchalik tez-tez o'zgaradi? Juda kamdan-kam hollarda, shuning uchun bir necha oy davomida biz bu qiymat statik bo'lishini aytishimiz mumkin. Va agar biror narsa o'zgarsa, biz testlarni yangilaymiz. Tanishish - JavaRushGroupClientTest:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfoType.TECH;

@DisplayName("Integration-level testing for JavaRushGroupClientImplTest")
class JavaRushGroupClientTest {

   private final JavaRushGroupClient groupClient = new JavaRushGroupClientImpl("https://javarush.com/api/1.0/rest");

   @Test
   public void shouldProperlyGetGroupsWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupsDiscWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetGroupDiscWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder().build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(30, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupTECHCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder()
               .type(TECH)
               .build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(7, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupById() {
       //given
       Integer androidGroupId = 16;

       //when
       GroupDiscussionInfo groupById = groupClient.getGroupById(androidGroupId);

       //then
       Assertions.assertNotNull(groupById);
       Assertions.assertEquals(16, groupById.getId());
       Assertions.assertEquals(TECH, groupById.getType());
       Assertions.assertEquals("android", groupById.getKey());
   }
}
Testlar avvalgi uslubda yozilgan. Har bir so'rov uchun bir nechta testlar mavjud. Hamma narsani sinab ko'rishning ma'nosi yo'q, chunki bu API allaqachon eng yaxshi tarzda sinovdan o'tgan deb o'ylayman.

Xulosa

Ushbu maqolaning bir qismi sifatida biz JavaRush API-ga guruhlar uchun Java mijozini qo'shdik. Ular aytganidek, yashang va o'rganing. Ushbu mijozni yozayotganimda, men ularning hujjatlaridan foydalandim va ular taqdim etgan ob'ektlar bilan ishlashdan qulay foydalandim. Men taklif qilgan vazifaga e'tiboringizni qarataman. Agar kimdir qiziqsa, menga shaxsiy xabar yozing, bu juda qiziqarli tajriba bo'lishiga ishonchim komil. Bu birinchi qism edi. Ikkinchisida biz to'g'ridan-to'g'ri qo'shish buyrug'ini amalga oshiramiz va (agar biz uni bitta maqolaga joylashtirsak) foydalanuvchi obuna bo'lgan guruhlar ro'yxatini olishni qo'shamiz. Keyin, kimda bot uchun matn yozish istagi va iste'dodi bo'lsa, iltimos, menga PM orqali yozing. Men bu masalada mutaxassis emasman va har qanday yordam juda foydali bo'ladi. Keling, bularning barchasini ochiq kodli dastur sifatida rasmiylashtiramiz, bu qiziqarli bo'ladi! Xo'sh, odatdagidek - yoqing, obuna bo'ling, qo'ng'iroq qiling , loyihamizga yulduz bering , sharhlar yozing va maqolani baholang!
foydali havolalar

Seriyadagi barcha materiallar ro'yxati ushbu maqolaning boshida.

Izohlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION