|
|
@@ -372,6 +372,94 @@ func (c *Client) ResolveGroupChannel(ctx context.Context, username string) (*tg.
|
|
|
return nil, nil, fmt.Errorf("无法解析群组为超级群组: %s", username)
|
|
|
}
|
|
|
|
|
|
+// GetFullChannelTotal calls channels.getFullChannel to obtain the authoritative
|
|
|
+// participants_count (which may be much larger than what a restricted member
|
|
|
+// can see via ChannelParticipantsSearch).
|
|
|
+func (c *Client) GetFullChannelTotal(ctx context.Context, ch *tg.InputChannel) (int, error) {
|
|
|
+ api, err := c.waitReady(ctx)
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ full, err := api.ChannelsGetFullChannel(ctx, ch)
|
|
|
+ if err != nil {
|
|
|
+ return 0, wrapFloodWait(err)
|
|
|
+ }
|
|
|
+ cf, ok := full.FullChat.(*tg.ChannelFull)
|
|
|
+ if !ok {
|
|
|
+ return 0, fmt.Errorf("unexpected FullChat type")
|
|
|
+ }
|
|
|
+ if n, ok := cf.GetParticipantsCount(); ok {
|
|
|
+ return n, nil
|
|
|
+ }
|
|
|
+ return 0, nil
|
|
|
+}
|
|
|
+
|
|
|
+// FetchRecentParticipants paginates channels.getParticipants with the Recent
|
|
|
+// filter, which returns active participants sorted by recency. This is the
|
|
|
+// preferred Phase 1 filter for large groups — empty-string Search filter is
|
|
|
+// often heavily restricted (returns 4-5 results) while Recent returns up to
|
|
|
+// ~200 and is not treated as "searching".
|
|
|
+func (c *Client) FetchRecentParticipants(ctx context.Context, channel *tg.InputChannel) ([]GroupParticipant, int, error) {
|
|
|
+ api, err := c.waitReady(ctx)
|
|
|
+ if err != nil {
|
|
|
+ return nil, 0, err
|
|
|
+ }
|
|
|
+
|
|
|
+ const pageSize = 200
|
|
|
+ offset := 0
|
|
|
+ totalCount := 0
|
|
|
+ var out []GroupParticipant
|
|
|
+
|
|
|
+ for {
|
|
|
+ result, err := api.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{
|
|
|
+ Channel: channel,
|
|
|
+ Filter: &tg.ChannelParticipantsRecent{},
|
|
|
+ Offset: offset,
|
|
|
+ Limit: pageSize,
|
|
|
+ Hash: 0,
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return out, totalCount, wrapFloodWait(err)
|
|
|
+ }
|
|
|
+ cp, ok := result.(*tg.ChannelsChannelParticipants)
|
|
|
+ if !ok || len(cp.Users) == 0 {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if cp.Count > totalCount {
|
|
|
+ totalCount = cp.Count
|
|
|
+ }
|
|
|
+ for _, u := range cp.Users {
|
|
|
+ user, ok := u.(*tg.User)
|
|
|
+ if !ok {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ p := GroupParticipant{
|
|
|
+ ID: user.GetID(),
|
|
|
+ IsBot: user.GetBot(),
|
|
|
+ IsPremium: user.GetPremium(),
|
|
|
+ }
|
|
|
+ if un, ok := user.GetUsername(); ok {
|
|
|
+ p.Username = un
|
|
|
+ }
|
|
|
+ if fn, ok := user.GetFirstName(); ok {
|
|
|
+ p.FirstName = fn
|
|
|
+ }
|
|
|
+ if ln, ok := user.GetLastName(); ok {
|
|
|
+ p.LastName = ln
|
|
|
+ }
|
|
|
+ out = append(out, p)
|
|
|
+ }
|
|
|
+ offset += len(cp.Users)
|
|
|
+ if offset >= cp.Count {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if err := jitterSleep(ctx, 800*time.Millisecond, 1500*time.Millisecond); err != nil {
|
|
|
+ return out, totalCount, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out, totalCount, nil
|
|
|
+}
|
|
|
+
|
|
|
// JoinChannel makes the current account a member of the given channel/supergroup.
|
|
|
// USER_ALREADY_PARTICIPANT is treated as success. FloodWait is wrapped normally.
|
|
|
// Side effect: this account becomes visibly a member of the group — make sure
|