Files
duplicati/Duplicati/Library/Backend/Telegram/TelegramBackend.cs
T

495 lines
19 KiB
C#

#region Disclaimer / License
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Interface;
using Duplicati.Library.Logging;
using TeleSharp.TL;
using TeleSharp.TL.Channels;
using TeleSharp.TL.Messages;
using TLSharp.Core;
using TLSharp.Core.Exceptions;
using TLSharp.Core.Network.Exceptions;
using TLSharp.Core.Utils;
using TLRequestDeleteMessages = TeleSharp.TL.Channels.TLRequestDeleteMessages;
namespace Duplicati.Library.Backend
{
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
public class Telegram : IBackend
{
private const int MEBIBYTE_IN_BYTES = 1048576;
private static readonly string LOG_TAG = Log.LogTagFromType(typeof(Telegram));
private static readonly InMemorySessionStore m_sessionStore = new InMemorySessionStore();
private static readonly object m_lockObj = new object();
private readonly int m_apiId;
private readonly string m_apiHash;
private readonly string m_authCode;
private readonly string m_password;
private readonly string m_channelName;
private readonly string m_phoneNumber;
private TLChannel m_channelCache;
private static TelegramClient m_telegramClient;
public Telegram()
{ }
public Telegram(string url, Dictionary<string, string> options)
{
if (options.TryGetValue(Strings.API_ID_KEY, out var apiId))
{
m_apiId = int.Parse(apiId);
}
if (options.TryGetValue(Strings.API_HASH_KEY, out var apiHash))
{
m_apiHash = apiHash.Trim();
}
if (options.TryGetValue(Strings.PHONE_NUMBER_KEY, out var phoneNumber))
{
m_phoneNumber = phoneNumber.Trim();
}
if (options.TryGetValue(Strings.AUTH_CODE_KEY, out var authCode))
{
m_authCode = authCode.Trim();
}
if (options.TryGetValue(Strings.AUTH_PASSWORD, out var password))
{
m_password = password.Trim();
}
if (options.TryGetValue(Strings.CHANNEL_NAME, out var channelName))
{
m_channelName = channelName.Trim();
}
if (m_apiId == 0)
{
throw new UserInformationException(Strings.NoApiIdError, nameof(Strings.NoApiIdError));
}
if (string.IsNullOrEmpty(m_apiHash))
{
throw new UserInformationException(Strings.NoApiHashError, nameof(Strings.NoApiHashError));
}
if (string.IsNullOrEmpty(m_phoneNumber))
{
throw new UserInformationException(Strings.NoPhoneNumberError, nameof(Strings.NoPhoneNumberError));
}
if (string.IsNullOrEmpty(m_channelName))
{
throw new UserInformationException(Strings.NoChannelNameError, nameof(Strings.NoChannelNameError));
}
InitializeTelegramClient(m_apiId, m_apiHash, m_phoneNumber);
}
private static void InitializeTelegramClient(int apiId, string apiHash, string phoneNumber)
{
var tmpTelegramClient = m_telegramClient;
m_telegramClient = new TelegramClient(apiId, apiHash, m_sessionStore, phoneNumber);
tmpTelegramClient?.Dispose();
}
public void Dispose()
{
// Do not dispose m_telegramClient because of "session-based" authentication.
}
public string DisplayName { get; } = Strings.DisplayName;
public string ProtocolKey { get; } = "https";
public IEnumerable<IFileEntry> List()
{
return SafeExecute<IEnumerable<IFileEntry>>(() =>
{
Authenticate();
EnsureChannelCreated();
var fileInfos = ListChannelFileInfos();
var result = fileInfos.Select(fi => fi.ToFileEntry());
return result;
},
nameof(List));
}
public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
{
return SafeExecute(() =>
{
var channel = GetChannel();
using (var fs = File.OpenRead(filename))
{
var fsReader = new StreamReader(fs);
cancelToken.ThrowIfCancellationRequested();
EnsureConnected();
var file = m_telegramClient.UploadFile(remotename, fsReader, cancelToken).GetAwaiter().GetResult();
cancelToken.ThrowIfCancellationRequested();
var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = (long)channel.AccessHash};
var fileNameAttribute = new TLDocumentAttributeFilename
{
FileName = remotename
};
EnsureConnected();
m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector<TLAbsDocumentAttribute> {fileNameAttribute}, cancelToken).GetAwaiter().GetResult();
fs.Close();
}
return Task.CompletedTask;
},
$"{nameof(PutAsync)}({remotename})");
}
public void Get(string remotename, string filename)
{
SafeExecute(() =>
{
var fileInfo = ListChannelFileInfos().First(fi => fi.Name == remotename);
var fileLocation = fileInfo.ToFileLocation();
var limit = MEBIBYTE_IN_BYTES;
var currentOffset = 0;
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
while (currentOffset < fileInfo.Size)
{
try
{
EnsureConnected();
var file = m_telegramClient.GetFile(fileLocation, limit, currentOffset).GetAwaiter().GetResult();
fs.Write(file.Bytes, 0, file.Bytes.Length);
currentOffset += file.Bytes.Length;
}
catch (InvalidOperationException e)
{
if (e.Message.Contains("Couldn't read the packet length") == false)
{
throw;
}
}
}
fs.Close();
}
},
$"{nameof(Get)}({remotename})");
}
public void Delete(string remotename)
{
SafeExecute(() =>
{
var channel = GetChannel();
var fileInfo = ListChannelFileInfos().FirstOrDefault(fi => fi.Name == remotename);
if (fileInfo == null)
{
return;
}
var request = new TLRequestDeleteMessages
{
Channel = new TLInputChannel
{
ChannelId = channel.Id,
AccessHash = channel.AccessHash.Value
},
Id = new TLVector<int> {fileInfo.MessageId}
};
EnsureConnected();
m_telegramClient.SendRequestAsync<TLAffectedMessages>(request).GetAwaiter().GetResult();
},
$"{nameof(Delete)}({remotename})");
}
public List<ChannelFileInfo> ListChannelFileInfos()
{
var channel = GetChannel();
var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = channel.AccessHash.Value};
var result = new List<ChannelFileInfo>();
var cMinId = (int?)0;
while (cMinId != null)
{
var newCMinId = RetrieveMessages(inputPeerChannel, result, cMinId.Value);
if (newCMinId == cMinId)
{
break;
}
cMinId = newCMinId;
}
result = result.Distinct().ToList();
return result;
}
private int? RetrieveMessages(TLInputPeerChannel inputPeerChannel, List<ChannelFileInfo> result, int maxDate)
{
EnsureConnected();
var absHistory = m_telegramClient.GetHistoryAsync(inputPeerChannel, offsetDate: maxDate).GetAwaiter().GetResult();
var history = ((TLChannelMessages)absHistory).Messages.OfType<TLMessage>();
var minDate = (int?)null;
foreach (var msg in history)
{
minDate = minDate == null || minDate > msg.Id ? msg.Date : minDate;
if (msg.Media is TLMessageMediaDocument media)
{
var mediaDoc = media.Document as TLDocument;
var fileInfo = new ChannelFileInfo(msg.Id, mediaDoc.AccessHash, mediaDoc.Id, mediaDoc.Version, mediaDoc.Size, media.Caption, DateTimeOffset.FromUnixTimeSeconds(msg.Date).UtcDateTime);
result.Add(fileInfo);
}
}
return minDate;
}
public IList<ICommandLineArgument> SupportedCommands { get; } = new List<ICommandLineArgument>
{
new CommandLineArgument(Strings.API_ID_KEY, CommandLineArgument.ArgumentType.Integer, Strings.ApiIdShort, Strings.ApiIdLong),
new CommandLineArgument(Strings.API_HASH_KEY, CommandLineArgument.ArgumentType.String, Strings.ApiHashShort, Strings.ApiHashLong),
new CommandLineArgument(Strings.PHONE_NUMBER_KEY, CommandLineArgument.ArgumentType.String, Strings.PhoneNumberShort, Strings.PhoneNumberLong),
new CommandLineArgument(Strings.AUTH_CODE_KEY, CommandLineArgument.ArgumentType.String, Strings.AuthCodeShort, Strings.AuthCodeLong),
new CommandLineArgument(Strings.AUTH_PASSWORD, CommandLineArgument.ArgumentType.String, Strings.PasswordShort, Strings.PasswordLong),
new CommandLineArgument(Strings.CHANNEL_NAME, CommandLineArgument.ArgumentType.String, Strings.ChannelNameShort, Strings.ChannelNameLong)
};
public string Description { get; } = Strings.Description;
public string[] DNSName { get; }
public void Test()
{
// No need to use our method
lock (m_lockObj)
{
Authenticate();
}
}
public void CreateFolder()
{
SafeExecute(() =>
{
Authenticate();
EnsureChannelCreated();
},
nameof(CreateFolder));
}
private TLChannel GetChannel()
{
if (m_channelCache != null)
{
return m_channelCache;
}
EnsureConnected();
var absDialogs = m_telegramClient.GetUserDialogsAsync().GetAwaiter().GetResult();
var userDialogs = absDialogs as TLDialogs;
var userDialogsSlice = absDialogs as TLDialogsSlice;
TLVector<TLAbsChat> absChats;
if (userDialogs != null)
{
absChats = userDialogs.Chats;
}
else
{
absChats = userDialogsSlice.Chats;
}
var channel = (TLChannel)absChats.FirstOrDefault(chat => chat is TLChannel tlChannel && tlChannel.Title == m_channelName);
m_channelCache = channel;
return channel;
}
private void EnsureChannelCreated()
{
var channel = GetChannel();
if (channel == null)
{
var newGroup = new TLRequestCreateChannel
{
Broadcast = false,
Megagroup = false,
Title = m_channelName,
About = string.Empty
};
EnsureConnected();
m_telegramClient.SendRequestAsync<object>(newGroup).GetAwaiter().GetResult();
}
}
private void Authenticate()
{
EnsureConnected();
if (IsAuthenticated())
{
return;
}
try
{
var phoneCodeHash = m_sessionStore.GetPhoneHash(m_phoneNumber);
if (phoneCodeHash == null)
{
EnsureConnected();
phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult();
m_sessionStore.SetPhoneHash(m_phoneNumber, phoneCodeHash);
m_telegramClient.Session.Save();
throw new UserInformationException(Strings.NoOrWrongAuthCodeError, nameof(Strings.NoOrWrongAuthCodeError));
}
m_telegramClient.MakeAuthAsync(m_phoneNumber, phoneCodeHash, m_authCode).GetAwaiter().GetResult();
}
catch (CloudPasswordNeededException)
{
if (string.IsNullOrEmpty(m_password))
{
m_telegramClient.Session.Save();
throw new UserInformationException(Strings.NoPasswordError, nameof(Strings.NoPasswordError));
}
EnsureConnected();
var passwordSetting = m_telegramClient.GetPasswordSetting().GetAwaiter().GetResult();
m_telegramClient.MakeAuthWithPasswordAsync(passwordSetting, m_password).GetAwaiter().GetResult();
}
m_telegramClient.Session.Save();
}
private void EnsureConnected()
{
var isConnected = false;
while (isConnected == false)
{
try
{
isConnected = m_telegramClient.ConnectAsync().Wait(TimeSpan.FromSeconds(5));
}
catch (FloodException floodExc)
{
var randSeconds = new Random().Next(0, 15);
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
Thread.Sleep(floodExc.TimeToWait + TimeSpan.FromSeconds(randSeconds));
}
catch (Exception e)
{
InitializeTelegramClient(m_apiId, m_apiHash, m_phoneNumber);
isConnected = false;
}
}
}
private bool IsAuthenticated()
{
var isAuthorized = m_telegramClient.IsUserAuthorized();
return isAuthorized;
}
private void SafeExecute(Action action, string actionName)
{
lock (m_lockObj)
{
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
try
{
action();
}
catch (UserInformationException)
{
throw;
}
catch (FloodException floodExc)
{
var randSeconds = new Random().Next(0, 15);
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
Thread.Sleep(floodExc.TimeToWait + TimeSpan.FromSeconds(randSeconds));
SafeExecute(action, actionName);
}
catch (Exception e)
{
Log.WriteErrorMessage(LOG_TAG, nameof(Strings.EXCEPTION_RETRY), e, Strings.EXCEPTION_RETRY);
action();
}
}
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
}
private T SafeExecute<T>(Func<T> func, string actionName)
{
lock (m_lockObj)
{
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
try
{
var res = func();
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
return res;
}
catch (UserInformationException)
{
throw;
}
catch (FloodException floodExc)
{
var randSeconds = new Random().Next(0, 15);
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
Thread.Sleep(floodExc.TimeToWait);
var res = SafeExecute(func, actionName);
Log.WriteInformationMessage(LOG_TAG, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
return res;
}
catch (Exception e)
{
Log.WriteErrorMessage(LOG_TAG, nameof(Strings.EXCEPTION_RETRY), e, Strings.EXCEPTION_RETRY);
return func();
}
}
}
}
}