feat(go): rewrite in Go

This commit is contained in:
tiennm99 committed 2025-11-28 22:25:09 +07:00
1 parent 6726701d87
commit eef026336c
9 files changed
+504 -265

No files matched your search

+27 -155
View File
@@ -1,164 +1,36 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.idea
# C extensions
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Test binary, built with `go test -c`
*.test
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Dependency directories (remove the comment below to include it)
# vendor/
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Go workspace file
go.work
go.work.sum
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
# env file
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
*.session
*.csv
# Editor/IDE
# .idea/
# .vscode/
+2 -24
View File
@@ -1,25 +1,3 @@
# export-telegram-group-members
# Export Telegram group members
Export Telegram group members
## How to use
1. Clone this repository:
```bash
git clone https://github.com/tiennm99/export-telegram-group-members.git
```
2. Install requirements:
```bash
pip install -r requirements.txt
```
3. Create a new Telegram app at [https://my.telegram.org](https://my.telegram.org) and get the `api_id` and `api_hash`.
4. Copy `.env.example` to `.env` and fill in the `api_id`, `api_hash`, and `phone`.
5. Run the script:
```bash
python main.py
```
*In 2025, I rewrote this project using Go. The original Python version of this project can be found at `feature/python` branch.*
-22
View File
@@ -1,22 +0,0 @@
import csv
import os
import re
from datetime import datetime
now = datetime.now()
folder_name = now.strftime('%Y-%m-%d %H-%M-%S')
os.mkdir(folder_name)
def remove_special_characters(string):
return re.sub('[<>:"/\\\\|?*]', '', string)
def export_csv(group_name, members):
filename = remove_special_characters(group_name) + '.csv'
path = os.path.join(folder_name, filename)
with open(path, 'w', encoding='UTF-8') as f:
writer = csv.writer(f, delimiter=',', lineterminator='\n')
writer.writerow(['id', 'username', 'first_name', 'last_name'])
for member in members:
writer.writerow([member.id, member.username, member.first_name, member.last_name])
-15
View File
@@ -1,15 +0,0 @@
import os
from dotenv import load_dotenv
load_dotenv()
phone = os.getenv('PHONE')
api_id = os.getenv('API_ID')
api_hash = os.getenv('API_HASH')
proxy_host = os.getenv('PROXY_HOST')
proxy_port = int(os.getenv('PROXY_PORT', '0')) if os.getenv('PROXY_PORT') else None
proxy_secret = os.getenv('PROXY_SECRET')
group_names_to_export = ['ZingPlay Game Studios', 'ZPS HCM', 'ZPS HCM - Xin nghỉ (phép/đi trễ)'] # Add the group names you want to export here
+27
View File
@@ -0,0 +1,27 @@
module github.com/tiennm99/export-telegram-group-members
go 1.21
require github.com/gotd/td v0.98.0
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-faster/jx v1.2.0 // indirect
github.com/go-faster/xor v1.0.0 // indirect
github.com/gotd/ige v0.2.2 // indirect
github.com/gotd/neo v0.1.5 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/segmentio/asm v1.2.1 // indirect
go.opentelemetry.io/otel v1.23.1 // indirect
go.opentelemetry.io/otel/trace v1.23.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect
rsc.io/qr v0.2.0 // indirect
)
+56
View File
@@ -0,0 +1,56 @@
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.98.0 h1:WNS6ob/Nl1OBskXiOO/JNxCO2j3zGG2oKqm/ELHDjQY=
github.com/gotd/td v0.98.0/go.mod h1:Px8r98qWVHjQ3l68PVBCJfK1+Y9ZaAIkflqx0mqKcAg=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0=
golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
+392
View File
@@ -0,0 +1,392 @@
package main
import (
"bufio"
"context"
"encoding/csv"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/gotd/td/telegram"
"github.com/gotd/td/telegram/auth"
"github.com/gotd/td/tg"
)
// Config holds the application configuration
type Config struct {
Phone string
APIId int
APIHash string
GroupNames []string
}
// Member represents a Telegram group member
type Member struct {
ID int64
Username string
FirstName string
LastName string
}
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func run() error {
// Load configuration
config := loadConfig()
ctx := context.Background()
// Setup client options
opts := telegram.Options{
SessionStorage: &telegram.FileSessionStorage{
Path: "session.json",
},
}
client := telegram.NewClient(config.APIId, config.APIHash, opts)
return client.Run(ctx, func(ctx context.Context) error {
// Authenticate
if err := authenticate(ctx, client, config.Phone); err != nil {
return fmt.Errorf("authentication failed: %w", err)
}
// Export group members
if err := exportGroupMembers(ctx, client, config.GroupNames); err != nil {
return fmt.Errorf("failed to export members: %w", err)
}
fmt.Println("\nExport completed successfully!")
return nil
})
}
// loadConfig loads configuration - modify these values
func loadConfig() *Config {
return &Config{
// TODO: Set your Telegram credentials here
Phone: "+1234567890", // Your phone number
APIId: 123456789, // Your API ID from https://my.telegram.org
APIHash: "your_api_hash_here", // Your API Hash from https://my.telegram.org
// Define groups to export
GroupNames: []string{
"ZingPlay Game Studios",
"ZPS HCM",
"ZPS HCM - Xin nghỉ (phép/đi trễ)",
},
}
}
// authenticate handles the Telegram authentication flow
func authenticate(ctx context.Context, client *telegram.Client, phone string) error {
api := client.API()
flow := auth.NewFlow(
&terminalAuth{phone: phone},
auth.SendCodeOptions{},
)
if err := client.Auth().IfNecessary(ctx, flow); err != nil {
return err
}
// Get current user to confirm authentication
user, err := api.UsersGetUsers(ctx, []tg.InputUserClass{&tg.InputUserSelf{}})
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
if len(user) > 0 {
if u, ok := user[0].(*tg.User); ok {
fmt.Printf("Authenticated as: %s %s\n", u.FirstName, u.LastName)
}
}
return nil
}
// terminalAuth implements auth flow for terminal input
type terminalAuth struct {
phone string
}
func (a *terminalAuth) Phone(_ context.Context) (string, error) {
return a.phone, nil
}
func (a *terminalAuth) Password(_ context.Context) (string, error) {
fmt.Print("Enter 2FA password: ")
password, err := readLine()
if err != nil {
return "", err
}
return strings.TrimSpace(password), nil
}
func (a *terminalAuth) Code(_ context.Context, _ *tg.AuthSentCode) (string, error) {
fmt.Print("Enter verification code: ")
code, err := readLine()
if err != nil {
return "", err
}
return strings.TrimSpace(code), nil
}
func (a *terminalAuth) AcceptTermsOfService(_ context.Context, tos tg.HelpTermsOfService) error {
return nil
}
func (a *terminalAuth) SignUp(_ context.Context) (auth.UserInfo, error) {
return auth.UserInfo{}, fmt.Errorf("signup not supported")
}
// readLine reads a line from stdin
func readLine() (string, error) {
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(line), nil
}
// exportGroupMembers exports members from specified groups
func exportGroupMembers(ctx context.Context, client *telegram.Client, groupNames []string) error {
api := client.API()
// Create timestamped directory for exports
timestamp := time.Now().Format("2006-01-02 15-04-05")
exportDir := timestamp
if err := os.MkdirAll(exportDir, 0755); err != nil {
return fmt.Errorf("failed to create export directory: %w", err)
}
fmt.Printf("\nExporting to directory: %s\n\n", exportDir)
// Get all dialogs
dialogs, err := getDialogs(ctx, api)
if err != nil {
return fmt.Errorf("failed to get dialogs: %w", err)
}
// Filter and process groups
groupsExported := 0
for _, dialog := range dialogs {
// Check if it's a channel/group
peer, ok := dialog.Peer.(*tg.PeerChannel)
if !ok {
continue
}
// Get channel info
var channelName string
for _, chat := range dialog.Chats {
if channel, ok := chat.(*tg.Channel); ok && channel.ID == peer.ChannelID {
channelName = channel.Title
break
}
}
// Check if this group should be exported
if !contains(groupNames, channelName) {
continue
}
fmt.Printf("Exporting group: %s\n", channelName)
// Get members
members, err := getChannelMembers(ctx, api, peer.ChannelID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get members for %s: %v\n", channelName, err)
continue
}
// Sort members by ID
sort.Slice(members, func(i, j int) bool {
return members[i].ID < members[j].ID
})
// Export to CSV
filename := sanitizeFilename(channelName) + ".csv"
filepath := filepath.Join(exportDir, filename)
if err := exportToCSV(filepath, members); err != nil {
fmt.Fprintf(os.Stderr, "Failed to export %s: %v\n", channelName, err)
continue
}
fmt.Printf(" -> Exported %d members to %s\n", len(members), filename)
groupsExported++
}
if groupsExported == 0 {
fmt.Println("No matching groups found to export.")
}
return nil
}
// getDialogs retrieves all dialogs (conversations)
func getDialogs(ctx context.Context, api *tg.Client) ([]*tg.Dialog, error) {
var allDialogs []*tg.Dialog
var offsetDate int
var offsetID int
var offsetPeer tg.InputPeerClass = &tg.InputPeerEmpty{}
for {
dialogs, err := api.MessagesGetDialogs(ctx, &tg.MessagesGetDialogsRequest{
OffsetDate: offsetDate,
OffsetID: offsetID,
OffsetPeer: offsetPeer,
Limit: 100,
})
if err != nil {
return nil, err
}
var dialogsList []*tg.Dialog
switch d := dialogs.(type) {
case *tg.MessagesDialogs:
dialogsList = d.Dialogs
allDialogs = append(allDialogs, d.Dialogs...)
// Last page reached
return allDialogs, nil
case *tg.MessagesDialogsSlice:
dialogsList = d.Dialogs
allDialogs = append(allDialogs, d.Dialogs...)
if len(d.Dialogs) == 0 {
return allDialogs, nil
}
default:
return allDialogs, nil
}
// Update offset for next iteration
if len(dialogsList) > 0 {
lastDialog := dialogsList[len(dialogsList)-1]
offsetDate = lastDialog.TopMessage
offsetID = lastDialog.TopMessage
offsetPeer = lastDialog.Peer
} else {
break
}
}
return allDialogs, nil
}
// getChannelMembers retrieves all members from a channel
func getChannelMembers(ctx context.Context, api *tg.Client, channelID int64) ([]Member, error) {
// Get channel input
channel := &tg.InputChannel{
ChannelID: channelID,
}
var members []Member
var offset int
for {
participants, err := api.ChannelsGetParticipants(ctx, &tg.ChannelsGetParticipantsRequest{
Channel: channel,
Filter: &tg.ChannelParticipantsRecent{},
Offset: offset,
Limit: 200,
})
if err != nil {
return nil, err
}
channelParticipants, ok := participants.(*tg.ChannelsChannelParticipants)
if !ok {
break
}
if len(channelParticipants.Users) == 0 {
break
}
// Extract member data
for _, u := range channelParticipants.Users {
if user, ok := u.(*tg.User); ok {
member := Member{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
}
if user.Username != "" {
member.Username = user.Username
}
members = append(members, member)
}
}
offset += len(channelParticipants.Users)
// Check if we've reached the end
if len(channelParticipants.Users) < 200 {
break
}
}
return members, nil
}
// exportToCSV exports members to a CSV file
func exportToCSV(filename string, members []Member) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Write header
if err := writer.Write([]string{"id", "username", "first_name", "last_name"}); err != nil {
return err
}
// Write member data
for _, member := range members {
record := []string{
strconv.FormatInt(member.ID, 10),
member.Username,
member.FirstName,
member.LastName,
}
if err := writer.Write(record); err != nil {
return err
}
}
return nil
}
// sanitizeFilename removes invalid characters from filename
func sanitizeFilename(name string) string {
// Remove characters that are invalid in Windows filenames
invalidChars := regexp.MustCompile(`[<>:"/\\|?*]`)
return invalidChars.ReplaceAllString(name, "")
}
// contains checks if a string is in a slice
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
-40
View File
@@ -1,40 +0,0 @@
import getpass
from telethon.errors import SessionPasswordNeededError
from telethon.sync import TelegramClient
import TelethonFakeTLS
from common import *
from config import *
if proxy_host and proxy_port and proxy_secret:
proxy = (proxy_host, proxy_port, proxy_secret)
connection = TelethonFakeTLS.ConnectionTcpMTProxyFakeTLS
client = TelegramClient(phone, api_id, api_hash, connection=connection, proxy=proxy)
else:
client = TelegramClient(phone, api_id, api_hash)
client.connect()
if not client.is_user_authorized():
client.send_code_request(phone)
try:
client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
client.sign_in(password=getpass.getpass())
client.start(phone)
dialogs = client.get_dialogs()
for dialog in dialogs:
if not dialog.is_group:
print('skip ' + dialog.name + ' because it is not a group.')
continue
if dialog.name not in group_names_to_export:
print('skip ' + dialog.name + ' because it is not in the list of group names to export.')
continue
print('exporting ' + dialog.name)
members = client.get_participants(dialog)
members.sort(key=lambda x: x.id)
export_csv(dialog.name, members)
print('export ' + dialog.name + ' done.')
-9
View File
@@ -1,9 +0,0 @@
cffi==1.17.1
cryptography==45.0.5
pyaes==1.6.1
pyasn1==0.6.0
pycparser==2.22
python-dotenv==1.0.1
rsa==4.9
Telethon==1.34.0
TelethonFakeTLS==2022.4.4.16.37.38.728726