This repository has been archived by the owner on Dec 2, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathretrieve.go
217 lines (201 loc) · 7.31 KB
/
retrieve.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package main
import (
"context"
"fmt"
"sort"
"strings"
"time"
log "github.com/go-pkgz/lgr"
"github.com/gotd/td/tg"
)
const requestLimit = 100 // should be between 1 and 100
// banUserInfo stores all the information about a user to ban
type banUserInfo struct {
userID int64
accessHash int64
joined time.Time
message string
username string
firstName string
lastName string
}
type channelParticipantInfo struct {
participantInfo *tg.ChannelParticipant
info *tg.User
}
type searchParams struct {
banTo time.Time
duration time.Duration
offset int
limit int
ignoreMessages bool
}
// retrieves users by for given period and write them to file in ./ban directory
func searchAndStoreUsersToBan(ctx context.Context, api *tg.Client, channel *tg.Channel, params searchParams) {
banFrom := params.banTo.Add(-params.duration)
log.Printf("[INFO] Looking for users to ban who joined in %s between %s and %s", params.duration, banFrom, params.banTo)
// Buffered channel with users to ban
nottyList := make(chan channelParticipantInfo, requestLimit)
go getChannelMembersWithinTimeframe(ctx, api, channel, banFrom, params.banTo, params.offset, params.limit, nottyList)
fileName := fmt.Sprintf("./ban/%s.users.csv", time.Now().Format("2006-01-02T15-04-05"))
usersToBan := getUsersInfo(ctx, api, channel, nottyList, params.ignoreMessages)
if len(usersToBan) == 0 {
log.Printf("[INFO] No users to ban found")
return
}
if err := writeUsersToFile(usersToBan, fileName); err != nil {
log.Printf("[ERROR] Error writing users to ban to file: %v", err)
} else {
log.Printf("[INFO] Success, users to ban written to %s", fileName)
log.Printf("[INFO] Please review, and to ban run same command with the following flag:")
log.Printf("[INFO] --ban-and-kick-filepath %s", fileName)
}
}
// getSingleUserStoreInfo retrieves userID and joined date for users in given period and pushes them to users channel,
// closes provided channel before returning, supposed to be run in goroutine.
// Uses provided offset: Telegram sort seems to be stable so once you established there are no droids here,
// you can just add offset to always start from the point after the filtered users.
func getChannelMembersWithinTimeframe(ctx context.Context, api *tg.Client, channel *tg.Channel, banFrom, banTo time.Time, offset, searchLimit int, users chan<- channelParticipantInfo) {
defer close(users)
for {
if searchLimit != 0 && offset >= searchLimit {
break
}
participants, err := api.ChannelsGetParticipants(ctx,
&tg.ChannelsGetParticipantsRequest{
Channel: channel.AsInput(),
Filter: &tg.ChannelParticipantsRecent{},
Limit: requestLimit,
Offset: offset,
})
offset += requestLimit
if err != nil {
log.Printf("[ERROR] Error getting channel participants: %v", err)
break
}
if len(participants.(*tg.ChannelsChannelParticipants).Participants) == 0 {
log.Printf("[INFO] No more users to process")
break
}
for _, participant := range participants.(*tg.ChannelsChannelParticipants).Participants {
if p, ok := participant.(*tg.ChannelParticipant); ok {
joinTime := time.Unix(int64(p.Date), 0)
if joinTime.After(banFrom) && joinTime.Before(banTo) {
// retrieve user info searches over all retrieved users in the latest bunch
// O(N^2) but N is small (100)
for _, u := range participants.(*tg.ChannelsChannelParticipants).GetUsers() {
if u.GetID() == p.GetUserID() {
// ignore error as then we couldn't do anything about it anyway
if user, ok := u.(*tg.User); ok {
// there is no point in writing to channel if we can't get user info
// as without access hash we can't ban user
users <- channelParticipantInfo{participantInfo: p, info: user}
}
break
}
}
}
}
}
log.Printf("[INFO] Processed %d users", offset)
}
}
// getUsersInfo retrieves extended user info for every user in given channel, as well as single message sent by such user
func getUsersInfo(ctx context.Context, api *tg.Client, channel *tg.Channel, users <-chan channelParticipantInfo, ignoreMessages bool) []banUserInfo {
var members []banUserInfo
// Do not check for ctx.Done() because then we could store existing data about the user as-is and write it to a file
// instead of dropping the information which we already retrieved. That is achieved by closing users channel.
for {
userToBan, ok := <-users
if !ok {
break
}
userInfoToStore := getSingleUserStoreInfo(ctx, api, channel, userToBan, ignoreMessages)
members = append(members, userInfoToStore)
}
log.Printf("[INFO] %d users found", len(members))
// sort members by joined date
sort.Slice(members, func(i, j int) bool {
return members[i].joined.Before(members[j].joined)
})
return members
}
// getSingleUserStoreInfo retrieves extended user information for given user and returns filled banUserInfo
func getSingleUserStoreInfo(ctx context.Context, api *tg.Client, channel *tg.Channel, userToBan channelParticipantInfo, ignoreMessages bool) banUserInfo {
joined := time.Unix(int64(userToBan.participantInfo.Date), 0)
userInfoToStore := banUserInfo{
userID: userToBan.participantInfo.UserID,
joined: joined,
}
userInfoStr := "user to ban"
if userToBan.info.Username != "" {
userInfoStr += fmt.Sprintf(" @%s (%s %s) joined %s",
userToBan.info.Username,
userToBan.info.FirstName,
userToBan.info.LastName,
joined)
} else {
userInfoStr += fmt.Sprintf(" %s %s joined %s",
userToBan.info.FirstName,
userToBan.info.LastName,
joined)
}
userInfoToStore.username = userToBan.info.Username
userInfoToStore.firstName = userToBan.info.FirstName
userInfoToStore.lastName = userToBan.info.LastName
userInfoToStore.accessHash = userToBan.info.AccessHash
var message string
if !ignoreMessages {
message = getSingeUserMessage(ctx, api, channel, userToBan.info.AsInputPeer())
userInfoToStore.message = message
if len([]rune(message)) > 50 {
message = string([]rune(message)[:45]) + "... (truncated)"
}
}
if message != "" {
userInfoStr += fmt.Sprintf(", last message: %s", strings.ReplaceAll(message, "\n", " "))
}
if message == "" && !ignoreMessages {
userInfoStr += ", no message found"
}
log.Printf("[INFO] %s", userInfoStr)
return userInfoToStore
}
// getSingeUserMessage retrieves single user (last?) message from given channel from Telegram API
func getSingeUserMessage(ctx context.Context, api *tg.Client, channel *tg.Channel, user tg.InputPeerClass) string {
var message string
messages, err := api.MessagesSearch(ctx, &tg.MessagesSearchRequest{
FromID: user,
Peer: channel.AsInputPeer(),
Filter: &tg.InputMessagesFilterEmpty{},
Limit: 1,
})
if err != nil {
log.Printf("[ERROR] Error retrieving user %s message: %v", user.String(), err)
return ""
}
if messages.Zero() {
return ""
}
var rawMessages []tg.MessageClass
switch v := messages.(type) {
case *tg.MessagesMessages:
rawMessages = v.Messages
case *tg.MessagesMessagesSlice:
rawMessages = v.Messages
case *tg.MessagesChannelMessages:
rawMessages = v.Messages
}
if len(rawMessages) == 1 {
switch v := rawMessages[0].(type) {
case *tg.Message:
message = v.GetMessage()
case *tg.MessageService:
message = v.String()
if _, ok := v.GetAction().(*tg.MessageActionChatAddUser); ok {
message = "[system] joining the channel"
}
}
}
return message
}