JavaRush /Blog Java /Random-VI /Chúng tôi đang thêm khả năng đăng ký một nhóm bài viết. (...
Roman Beekeeper
Mức độ

Chúng tôi đang thêm khả năng đăng ký một nhóm bài viết. (Phần 1) - "Dự án Java từ A đến Z"

Xuất bản trong nhóm
Xin chào! Hôm nay chúng ta sẽ thêm mục đăng ký vào một nhóm bài viết trong JavaRush. Điều này tương ứng với vấn đề JRTB-5 trên GitHub. Hãy để tôi giải thích: JavaRush có một phần tên là Articles và trong đó có các Nhóm bài viết. Ý tưởng là nhận thông báo về một bài viết mới từ một hoặc nhiều nhóm thông qua bot telegram.“Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 1

Thêm JRTB-5

Giả sử tôi quan tâm đến các bài viết từ nhóm Câu chuyện thành công . Vì vậy, tôi muốn đăng ký nhận thông tin cập nhật từ nhóm này và luôn nhận được liên kết đến ấn phẩm mới. Là một phần của nhiệm vụ này, chúng ta cần tìm hiểu cách sử dụng API mở để làm việc với các nhóm trong JavaRush. Đúng lúc này một chuyện như vậy đã đến. Đây là liên kết đến mô tả về API mở .
Bạn! Bạn có muốn biết ngay khi nào mã mới cho một dự án hoặc một bài viết mới được phát hành không? Tham gia kênh tg của tôi . Ở đó tôi thu thập các bài viết, suy nghĩ của mình và quá trình phát triển nguồn mở cùng nhau.

vênh váo là gì? Hãy cùng tìm hiểu ngay bây giờ

Chúng ta vẫn chưa nói về sự vênh vang. Đối với những người chưa biết, tôi sẽ giải thích ngắn gọn: đây là nơi bạn có thể xem xét API của máy chủ một cách công khai và cố gắng thực hiện một số yêu cầu đối với nó. Thông thường, một nhóm vênh vang có thể yêu cầu. Trong trường hợp của chúng tôi, có ba nhóm: forum-question , group , post . Trong mỗi nhóm sẽ có một hoặc nhiều yêu cầu chỉ ra tất cả dữ liệu cần thiết để xây dựng yêu cầu này (nghĩa là những tham số bổ sung nào có thể được chuyển, phải làm gì với chúng, phương thức http nào, v.v.). Tôi khuyên các bạn nên đọc và xem thêm về chủ đề này, vì đây là một phần của quá trình phát triển mà hầu hết các bạn sẽ gặp phải. Để tìm hiểu, chúng ta hãy tìm hiểu xem có bao nhiêu nhóm trong JavaRush. Để thực hiện việc này, hãy mở rộng nhóm bộ điều khiển nhóm và chọn Nhận yêu cầu /api/1.0/rest/groups/count . Nó sẽ trả về cho chúng ta số lượng nhóm trong JavaRush. Hãy xem: “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 2Hình ảnh cho thấy truy vấn này hỗ trợ một số tham số (truy vấn, loại, bộ lọc). Để thử yêu cầu này, bạn cần tìm nút Dùng thử , sau đó có thể định cấu hình các tham số này: “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 3Bạn cũng có thể định cấu hình loại, bộ lọc và truy vấn ở đó (điều này thực sự thú vị ở đây: đây sẽ là tìm kiếm văn bản trong một nhóm). Nhưng bây giờ, hãy chạy nó mà không có bất kỳ hạn chế nào và xem có bao nhiêu nhóm trong JavaRush. Để thực hiện việc này, hãy nhấp vào Thực thi. Ngay bên dưới sẽ có phản hồi (trong phần Phản hồi của máy chủ) cho yêu cầu này: “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 4Chúng tôi thấy rằng có tổng cộng 30 nhóm , yêu cầu này được hoàn thành trong 95ms và có một số tiêu đề trong phản hồi. Tiếp theo, hãy thử cấu hình một số thông số. Hãy chọn tham số loại bằng giá trị COMPANY và xem kết quả thay đổi như thế nào: “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 5Có 4 tham số, làm thế nào để kiểm tra điều này? Thật dễ dàng: bạn có thể truy cập trang web, tìm phần bài viết, chọn tất cả các nhóm và thêm bộ lọc thích hợp vào đó ( https://javarush.com/groups/all?type=COMPANY ). Và vâng, thực sự thì chỉ có 4. Mặc dù thực tế là có 3 :D “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 6Cho đến nay nó vẫn phù hợp. Nhân tiện, nếu chúng tôi kiểm tra các trường đại học thì vẫn chưa có trường nào cả. Để giải trí, hãy xem điều gì sẽ xảy ra nếu bạn đặt filter = MY trong trình duyệt nơi bạn đã đăng nhập vào Javarush và chưa đăng nhập. Thông tin thêm về sự vênh váo - trong bài viết này trên Habré .

Viết ứng dụng khách cho API Javarush cho các nhóm

Bây giờ, dựa trên API mở, chúng ta sẽ viết một ứng dụng khách Java có thể đưa ra yêu cầu, nhận phản hồi và biết chính xác đối tượng nào sẽ đến. Chúng tôi cũng sẽ lấy các đối tượng từ phần vênh vang, từ phần Mô hình (ở cuối trang). Hãy tạo một gói mới và gọi nó là javarushclient bên cạnh dịch vụ, kho lưu trữ. Trong tương lai, chúng tôi sẽ chuyển thư viện này vào một thư viện riêng trong tổ chức Cộng đồng Javarush và sẽ sử dụng nó độc quyền như một thư viện phụ thuộc.
Tôi đã viết về cách tạo ứng dụng khách Java trong bài viết “Hướng dẫn tạo ứng dụng khách cho API Skyscanner và xuất bản nó trong jCenter và Maven Central” .
Trước hết, bạn cần thêm Unitrest, một thư viện để tạo các yêu cầu http vào API JavaRush:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
Và đặt phiên bản vào khối thuộc tính:
<unirest.version>3.11.01</unirest.version>
Sau khi có phần phụ thuộc, chúng ta có thể bắt đầu thêm mã. Hãy tạo một ứng dụng khách cho các nhóm JavaRushGroupClient và triển khai trong lớp JavaRushGroupClientImpl. Nhưng trước tiên, bạn cần tạo DTO (đối tượng truyền dữ liệu) - nghĩa là các lớp có đối tượng sẽ mang tất cả dữ liệu cần thiết cho máy khách. Tất cả các mô hình có thể được xem trong phần vênh vang. Ở phía dưới cùng có một phần Mô hình , trong đó bạn có thể đếm chúng. Đây là giao diện của GroupDiscussionInfo trong phần vênh vang: Trong gói javarushclient, chúng tôi sẽ tạo một “Dự án Java từ A đến Z”: Thêm tính năng đăng ký theo dõi một nhóm bài viết.  Phần 1 - 7gói dto mà chúng tôi sẽ thêm vào, dựa trên dữ liệu từ phần vênh vang, các lớp sau:
  • MeGroupInfoStatus :

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

  • Thông tin nhóm của tôi :

    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
    }

  • Thông tin thảo luận của người dùng :

    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;
    }

  • GroupVisibilitydtatus :

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

  • Sau đó - 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;
    }

GroupInfoGroupDiscusionInfo gần như giống nhau hoàn toàn nên hãy liên kết chúng theo tính kế thừa - 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;
}
Chúng ta cũng sẽ cần một bộ lọc cho yêu cầu GroupFilter :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

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

   UNKNOWN, MY, ALL
}
Trong yêu cầu lấy theo ID, nó trả về GroupDiscussionInfo và trong yêu cầu thu thập các nhóm, bạn có thể nhận cả GroupInfo và GroupDiscussionInfo. Vì các yêu cầu có thể có loại, truy vấn, bộ lọc, độ lệch và giới hạn, hãy tạo một lớp GroupRequestArgs riêng và biến nó thành lớp trình tạo (đọc mẫu trình tạo là gì):
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;
   }
}
Để tìm kiếm số lượng nhóm, nó hơi khác một chút. Nó chỉ có truy vấn, loại và bộ lọc. Và có vẻ như bạn không muốn sao chép mã. Đồng thời, nếu bạn bắt đầu kết hợp chúng, nó sẽ trở nên xấu xí khi làm việc với các nhà xây dựng. Vì vậy, tôi quyết định tách chúng ra và lặp lại mã. GroupCountRequestArgs trông như thế này :
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;
   }
}
Có, tôi chưa đề cập rằng hai lớp cuối cùng có phương thức populateQueries, phương thức này sẽ chuẩn bị bản đồ để tạo truy vấn (bạn sẽ thấy nó sau). Dựa trên các lớp được mô tả ở trên, hãy tạo giao diện cho JavaRushGroupClient :
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);
}
Hai yêu cầu khác nhau cho trường hợp chúng tôi muốn thêm thông tin GroupInfo hoặc GroupDiscussionInfo. Mặt khác, các truy vấn này giống hệt nhau và điểm khác biệt duy nhất là ở một truy vấn, cờ includeDiscussion sẽ đúng và ở truy vấn còn lại sẽ là sai. Do đó, có 4 phương pháp chứ không phải ba. Bây giờ chúng ta bắt đầu thực hiện:
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();
   }


}
Tôi thêm đường dẫn đến API trong hàm tạo bằng cách sử dụng chú thích Giá trị quen thuộc. Nó ngụ ý rằng giá trị bên trong chú thích tương ứng với một trường trong tệp thuộc tính. Vì vậy, hãy thêm một dòng mới vào application.properties:
javarush.api.path=https://javarush.com/api/1.0/rest
Giá trị này hiện sẽ ở một nơi cho tất cả ứng dụng khách API và nếu đường dẫn API thay đổi, chúng tôi sẽ nhanh chóng cập nhật giá trị đó. Trước đây, tôi đóng đinh bằng kính hiển vi, nhận được phản hồi từ yêu cầu http qua Unirest, dịch nó thành một chuỗi rồi phân tích chuỗi này thông qua Jackson... Thật đáng sợ, tẻ nhạt và đòi hỏi nhiều thứ bổ sung. Trong thư viện này bạn có thể thấy nó trông như thế nào. Ngay khi bắt tay vào sử dụng, tôi sẽ cấu trúc lại mọi thứ.
Bất kỳ ai muốn thử cập nhật thư viện này - chỉ thêm các đối tượng nhận bằng cách sử dụng các công cụ của thư viện unirest - viết bằng tin nhắn cá nhân hoặc dưới dạng một vấn đề mới trong chính thư viện. Đây sẽ là trải nghiệm làm việc thực sự cho bạn, nhưng tôi không bận tâm. Tôi sẽ tiến hành đánh giá mã đầy đủ và trợ giúp nếu cần thiết.
Bây giờ câu hỏi là: mã của chúng ta có hoạt động như chúng ta mong đợi không? Câu trả lời rất dễ: bạn chỉ cần viết bài kiểm tra cho họ. Như tôi đã nói nhiều lần, các nhà phát triển phải có khả năng viết bài kiểm thử. Do đó, bằng cách sử dụng Swagger UI, chúng tôi sẽ gửi yêu cầu, xem xét phản hồi và thay thế chúng vào các thử nghiệm như kết quả mong đợi. Bạn có thể nhận thấy ngay rằng số lượng nhóm không cố định và có thể thay đổi. Và bạn đã đúng. Câu hỏi duy nhất là con số này có thường xuyên thay đổi không? Rất hiếm khi xảy ra, vì vậy trong vài tháng, chúng tôi có thể nói rằng giá trị này sẽ không đổi. Và nếu có gì đó thay đổi, chúng tôi sẽ cập nhật các bài kiểm tra. Gặp gỡ - 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());
   }
}
Các bài kiểm tra được viết theo phong cách tương tự như trước đây. Có một số bài kiểm tra cho mỗi yêu cầu. Không có ích gì khi thử nghiệm mọi thứ vì tôi nghĩ rằng API này đã được thử nghiệm theo cách tốt nhất.

Phần kết luận

Là một phần của bài viết này, chúng tôi đã thêm ứng dụng khách Java dành cho nhóm vào API JavaRush. Như họ nói, hãy sống và học hỏi. Trong khi viết cho khách hàng này, tôi đã tận dụng tài liệu của họ và sử dụng công việc một cách thuận tiện với các đối tượng mà họ cung cấp. Tôi thu hút sự chú ý của bạn vào nhiệm vụ mà tôi đề xuất. Nếu ai quan tâm, hãy viết tin nhắn riêng cho tôi, tôi chắc chắn rằng đó sẽ là một trải nghiệm rất thú vị. Đây là phần đầu tiên. Trong phần thứ hai, chúng tôi sẽ trực tiếp thực hiện lệnh thêm và (nếu chúng tôi đưa nó vào một bài viết), chúng tôi sẽ thêm danh sách các nhóm mà người dùng đã đăng ký. Tiếp theo, bất kỳ ai có mong muốn và tài năng viết văn bản cho bot, vui lòng viết thư cho tôi qua PM. Tôi không phải là chuyên gia trong vấn đề này và bất kỳ trợ giúp nào cũng sẽ rất hữu ích. Hãy chính thức hóa tất cả những điều này dưới dạng phát triển nguồn mở, nó sẽ rất thú vị! Chà, như thường lệ - thích, đăng ký, bấm chuông , cho dự án của chúng tôi một ngôi sao , viết bình luận và đánh giá bài viết!
Liên kết hữu ích

Danh sách tất cả các tài liệu trong loạt bài này nằm ở đầu bài viết này.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION