mirror of
https://github.com/tiennm99/telegram-exporter.git
synced 2026-10-11 03:13:49 +00:00
rclone does not address a file by the bytes os.OpenFile wrote. Names handed to an Fs go through the backend encoder and names listed back are re-encoded to the standard set, neither of which the write path performs. A filename holding one of the rewritten characters was therefore stored under one string and looked up under another: confirmed against the local backend, where a written "a‛b.jpg" reports object not found and a written "a\nb.jpg" lists back as "a␊b.jpg". That is the same two-derivation divergence this program was written to remove, with rclone's encoder standing where filenamify used to. Such names are rejected, not encoded, for the same reason every other name is. The length limit ignored the ".part" suffix that is opened first, so a name just inside NAME_MAX passed the check and then failed to open on every pass, stalling the walk on that message forever. The suffix now lives beside the limit that has to account for it. filter.NewFilter(nil) does not build a neutral filter; it copies the package global, which rclone has already filled from RCLONE_*. Indexing inherited the operator's environment, so a stray RCLONE_MIN_SIZE emptied the index and re-downloaded the archive. Every narrowing field is now set explicitly and the result is asserted inactive. A subprocess test covers it, since the env is read at package init and t.Setenv is too late to observe anything. An ErrorDirNotFound from a subdirectory was also treated as an empty destination, returning a partial index as authoritative. t.me/c/<id> and t.me/s/<name> were passed through whole, and gotd reads the first path component as the username — resolving "c" or "s", which is a confusing failure at best and someone else's chat at worst, since one-character usernames exist. Both now yield the chat, and t.me/s/<name>/<id> is refused like any other message link. Two tests asserted the old behaviour. core's dcpool.Takeout deadlocks when takeout init fails: it holds the pool mutex and recovers by calling Client, which takes the same non-reentrant mutex. Telegram returns TAKEOUT_INIT_DELAY for a takeout started recently and takeout is on by default, so two runs in succession hang the process with no output and no response to cancellation. The session is established once here instead, falling back to a plain client, and the pool's own Takeout is never called.
110 lines
4.2 KiB
Go
110 lines
4.2 KiB
Go
package tgsource
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Every form the shell pipeline accepted must still be accepted, and the one
|
|
// form it refused must still be refused. An operator's existing command lines
|
|
// are the compatibility surface here.
|
|
func TestNormalizeChat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"numeric mtproto id", "1234567890", "1234567890"},
|
|
{"username with at", "@mychannel", "mychannel"},
|
|
{"bare username", "mychannel", "mychannel"},
|
|
{"public t.me link", "https://t.me/mychannel", "https://t.me/mychannel"},
|
|
{"tg protocol link", "tg://resolve?domain=mychannel", "tg://resolve?domain=mychannel"},
|
|
{"bot api id loses the -100 prefix", "-1001234567890", "1234567890"},
|
|
{"surrounding whitespace is trimmed", " mychannel\n", "mychannel"},
|
|
// The "c" and "s" markers are not the chat. Passed through whole, gotd
|
|
// takes the first path component as the username and resolves "c" or
|
|
// "s" — a confusing failure at best, and somebody else's chat at worst,
|
|
// since one-character usernames exist.
|
|
{"private channel link yields the channel id", "https://t.me/c/1234567890", "1234567890"},
|
|
{"preview link yields the username", "https://t.me/s/mychannel", "mychannel"},
|
|
{"preview link keeps the username's case", "https://t.me/s/MyChannel", "MyChannel"},
|
|
// Invite links are chats; their second component is not a bare message
|
|
// number and must not be read as one.
|
|
{"invite link", "https://t.me/+AbCd_1234", "https://t.me/+AbCd_1234"},
|
|
{"joinchat link", "https://t.me/joinchat/AbCd1234", "https://t.me/joinchat/AbCd1234"},
|
|
{"other telegram host, no message", "https://telegram.dog/mychannel", "https://telegram.dog/mychannel"},
|
|
{"tg link without a post parameter", "tg://resolve?domain=mychannel", "tg://resolve?domain=mychannel"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NormalizeChat(tt.in)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeChat(%q) returned an error: %v", tt.in, err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("NormalizeChat(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A message link names one message, not a chat. Guessing the chat from it would
|
|
// quietly export something the operator did not ask for, so it is refused.
|
|
func TestNormalizeChatRejectsMessageLinks(t *testing.T) {
|
|
for _, in := range []string{
|
|
"https://t.me/c/1234567890/4242",
|
|
"t.me/c/1234567890/4242",
|
|
"https://t.me/s/mychannel/4242",
|
|
"https://t.me/mychannel/4242",
|
|
// gotd accepts all three Telegram hosts and its parser keeps only the
|
|
// domain, silently dropping the message number — so missing one of these
|
|
// would walk an entire chat when one message was asked for.
|
|
"https://telegram.me/mychannel/4242",
|
|
"https://telegram.dog/mychannel/4242",
|
|
"https://T.ME/mychannel/4242",
|
|
// tg:// names the message in a query parameter, not the path.
|
|
"tg://privatepost?channel=1234567890&post=4242",
|
|
"tg://resolve?domain=mychannel&post=4242",
|
|
"tg://openmessage?user_id=1&message_id=4242",
|
|
} {
|
|
t.Run(in, func(t *testing.T) {
|
|
_, err := NormalizeChat(in)
|
|
if err == nil {
|
|
t.Fatalf("NormalizeChat(%q) succeeded; a message link is not a chat", in)
|
|
}
|
|
if !strings.Contains(err.Error(), "message link") {
|
|
t.Errorf("error should say it is a message link, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeChatRejectsEmpty(t *testing.T) {
|
|
for _, in := range []string{"", " ", "\t\n"} {
|
|
if _, err := NormalizeChat(in); err == nil {
|
|
t.Errorf("NormalizeChat(%q) succeeded, want an error", in)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -100 is stripped only when it prefixes a Bot API id, never from an ordinary
|
|
// number that happens to start with those digits.
|
|
func TestNormalizeChatOnlyStripsRealBotAPIPrefix(t *testing.T) {
|
|
tests := map[string]string{
|
|
"-1001234567890": "1234567890", // Bot API id
|
|
"1001234567890": "1001234567890", // no leading '-', not a Bot API id
|
|
"-100": "-100", // prefix with no id after it
|
|
"-2001234567890": "-2001234567890", // different prefix
|
|
}
|
|
for in, want := range tests {
|
|
got, err := NormalizeChat(in)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeChat(%q): %v", in, err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("NormalizeChat(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|