Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwarwickmm <warwickmm@users.noreply.github.com>2020-11-09 04:59:37 +0300
committerGitHub <noreply@github.com>2020-11-09 04:59:37 +0300
commitc7f9f8f5566b99d3189d9d32f24a888a8cdde231 (patch)
tree7a6128c2a2e3dcd8fb34e4c664020ec6905a99fa /Duplicati/Library
parent85139f86f243db02671b2fd527545e43a285c8e9 (diff)
parent812d698abab1cb8d1a663c23fd5aa7b50d89dde9 (diff)
Merge pull request #4324 from martikyan/feature/telegram_backend
Add support for Telegram channels as a backend destination.
Diffstat (limited to 'Duplicati/Library')
-rw-r--r--Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs86
-rw-r--r--Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj104
-rw-r--r--Duplicati/Library/Backend/Telegram/Duplicati.snkbin0 -> 596 bytes
-rw-r--r--Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs159
-rw-r--r--Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs48
-rw-r--r--Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs53
-rw-r--r--Duplicati/Library/Backend/Telegram/StreamReadHelper.cs48
-rw-r--r--Duplicati/Library/Backend/Telegram/Strings.cs63
-rw-r--r--Duplicati/Library/Backend/Telegram/TelegramBackend.cs521
-rw-r--r--Duplicati/Library/Backend/Telegram/app.config6
-rw-r--r--Duplicati/Library/Backend/Telegram/packages.config5
11 files changed, 1093 insertions, 0 deletions
diff --git a/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs b/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs
new file mode 100644
index 000000000..2233a9a93
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs
@@ -0,0 +1,86 @@
+using System;
+using Duplicati.Library.Common.IO;
+using TeleSharp.TL;
+
+namespace Duplicati.Library.Backend
+{
+ public class ChannelFileInfo : IEquatable<ChannelFileInfo>
+ {
+ public int MessageId { get; }
+ public long MediaDocAccessHash { get; }
+ public long DocumentId { get; }
+ public int Version { get; }
+ public long Size { get; }
+ public string Name { get; }
+ public DateTime Date { get; }
+
+ public ChannelFileInfo()
+ { }
+
+ public ChannelFileInfo(int messageId, long mediaDocAccessHash, long documentId, int version, long size, string name, DateTime date)
+ {
+ MessageId = messageId;
+ MediaDocAccessHash = mediaDocAccessHash;
+ DocumentId = documentId;
+ Version = version;
+ Size = size;
+ Name = name;
+ Date = date;
+ }
+
+ public FileEntry ToFileEntry()
+ {
+ return new FileEntry(Name, Size)
+ {
+ LastModification = Date,
+ LastAccess = Date,
+ IsFolder = false
+ };
+ }
+
+ public TLInputDocumentFileLocation ToFileLocation()
+ {
+ return new TLInputDocumentFileLocation
+ {
+ Id = DocumentId,
+ Version = Version,
+ AccessHash = MediaDocAccessHash
+ };
+ }
+
+ public bool Equals(ChannelFileInfo other)
+ {
+ if (ReferenceEquals(null, other))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return MessageId == other.MessageId;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj) || obj is ChannelFileInfo other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return MessageId;
+ }
+
+ public static bool operator ==(ChannelFileInfo left, ChannelFileInfo right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(ChannelFileInfo left, ChannelFileInfo right)
+ {
+ return !Equals(left, right);
+ }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj
new file mode 100644
index 000000000..7e8c7f6c9
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="15.0">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{B6BB37DE-1DCA-401B-B92F-202F341916EA}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Duplicati.Library.Backend</RootNamespace>
+ <AssemblyName>Duplicati.Library.Backend.Telegram</AssemblyName>
+ <AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
+ <FileUpgradeFlags>
+ </FileUpgradeFlags>
+ <OldToolsVersion>3.5</OldToolsVersion>
+ <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
+ <TargetFrameworkProfile />
+ <UpgradeBackupLocation>
+ </UpgradeBackupLocation>
+ <UseMSBuildEngine>false</UseMSBuildEngine>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="SharpAESCrypt, Version=1.3.3.0, Culture=neutral, PublicKeyToken=null">
+ <HintPath>..\..\..\..\packages\SharpAESCrypt.exe.1.3.3\lib\netstandard2.0\SharpAESCrypt.exe</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http" />
+ <Reference Include="TeleSharp.TL, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+ <HintPath>..\..\..\..\packages\TLSharp.0.1.0.574\lib\net46\TeleSharp.TL.dll</HintPath>
+ </Reference>
+ <Reference Include="TLSharp.Core, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+ <HintPath>..\..\..\..\packages\TLSharp.0.1.0.574\lib\net46\TLSharp.Core.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ChannelFileInfo.cs" />
+ <Compile Include="EncryptedFileSessionStore.cs" />
+ <Compile Include="Extensions\TelegramClientExtensions.cs" />
+ <Compile Include="StreamReadHelper.cs" />
+ <Compile Include="TelegramBackend.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Strings.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
+ <Project>{7e119745-1f62-43f0-936c-f312a1912c0b}</Project>
+ <Name>Duplicati.Library.AutoUpdater</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Logging\Duplicati.Library.Logging.csproj">
+ <Project>{d10a5fc0-11b4-4e70-86aa-8aea52bd9798}</Project>
+ <Name>Duplicati.Library.Logging</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
+ <Project>{52826615-7964-47FE-B4B3-1B2DBDF605B9}</Project>
+ <Name>Duplicati.Library.Modules.Builtin</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj">
+ <Project>{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}</Project>
+ <Name>Duplicati.Library.Utility</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj">
+ <Project>{C5899F45-B0FF-483C-9D38-24A9FCAAB237}</Project>
+ <Name>Duplicati.Library.Interface</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj">
+ <Project>{B68F2214-951F-4F78-8488-66E1ED3F50BF}</Project>
+ <Name>Duplicati.Library.Localization</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Common\Duplicati.Library.Common.csproj">
+ <Project>{D63E53E4-A458-4C2F-914D-92F715F58ACF}</Project>
+ <Name>Duplicati.Library.Common</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="app.config" />
+ <None Include="Duplicati.snk" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/Duplicati.snk b/Duplicati/Library/Backend/Telegram/Duplicati.snk
new file mode 100644
index 000000000..e0c1e2dd8
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/Duplicati.snk
Binary files differ
diff --git a/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs
new file mode 100644
index 000000000..5f0016874
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Duplicati.Library.AutoUpdater;
+using TLSharp.Core;
+
+namespace Duplicati.Library.Backend
+{
+ public class EncryptedFileSessionStore : ISessionStore
+ {
+ private static readonly object m_lockObj = new object();
+ private static readonly uint[] m_lookup32 = CreateLookup32();
+
+ private readonly string m_teleDataPath;
+ private readonly string m_password;
+ private readonly SHA256 m_sha = SHA256.Create();
+ private static readonly ConcurrentDictionary<string, byte[]> m_userIdLastSessionMap = new ConcurrentDictionary<string, byte[]>();
+
+ public EncryptedFileSessionStore(string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ throw new ArgumentNullException(nameof(password));
+ }
+
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var appName = AutoUpdateSettings.AppName;
+ m_teleDataPath = Path.Combine(appData, appName, nameof(Telegram));
+ m_password = password;
+
+ Directory.CreateDirectory(m_teleDataPath);
+ }
+
+
+ public void Save(Session session)
+ {
+ if (session.AuthKey == null)
+ {
+ return;
+ }
+
+ var sessionId = session.SessionUserId;
+ var filePath = GetSessionFilePath(sessionId);
+ var sessionBytes = session.ToBytes();
+
+ if (m_userIdLastSessionMap.TryGetValue(sessionId, out var sessionCache))
+ {
+ if (sessionCache.SequenceEqual(sessionBytes))
+ {
+ return;
+ }
+ }
+
+ WriteToEncryptedStorage(sessionBytes, m_password, filePath, sessionId);
+ }
+
+ public Session Load(string userId)
+ {
+ if (m_userIdLastSessionMap.TryGetValue(userId, out var cachedBytes))
+ {
+ var cachedSession = Session.FromBytes(cachedBytes, this, userId);
+ return cachedSession;
+ }
+
+ var filePath = GetSessionFilePath(userId);
+ var sessionBytes = ReadFromEncryptedStorage(m_password, filePath);
+
+ if (sessionBytes == null)
+ {
+ return null;
+ }
+
+ var session = Session.FromBytes(sessionBytes, this, userId);
+ return session;
+ }
+
+ private string GetSessionFilePath(string userId)
+ {
+ userId = userId.TrimStart('+');
+ var sha = GetShortSha(userId);
+ var sessionFilePath = Path.Combine(m_teleDataPath, $"t_{sha}.dat");
+
+ return sessionFilePath;
+ }
+
+ private static void WriteToEncryptedStorage(byte[] bytesToWrite, string pass, string path, string sessionId)
+ {
+ lock (m_lockObj)
+ {
+ using (var sessionMs = new MemoryStream(bytesToWrite))
+ using (var file = File.Open(path, FileMode.Create, FileAccess.Write))
+ {
+ SharpAESCrypt.SharpAESCrypt.Encrypt(pass, sessionMs, file);
+ }
+
+ m_userIdLastSessionMap[sessionId] = bytesToWrite;
+ }
+ }
+
+ private byte[] ReadFromEncryptedStorage(string pass, string path)
+ {
+ var fileInfo = new FileInfo(path);
+ if (fileInfo.Exists == false || fileInfo.Length == 0)
+ {
+ return null;
+ }
+
+ lock (m_lockObj)
+ {
+ using (var sessionMs = new MemoryStream())
+ using (var file = File.Open(path, FileMode.Open, FileAccess.Read))
+ {
+ SharpAESCrypt.SharpAESCrypt.Decrypt(pass, file, sessionMs);
+ return sessionMs.ToArray();
+ }
+ }
+ }
+
+ private string GetShortSha(string input)
+ {
+ var inputBytes = Encoding.UTF8.GetBytes(input);
+
+ var longShaBytes = m_sha.ComputeHash(inputBytes);
+ var longSha = ByteArrayToHexViaLookup32(longShaBytes);
+ var result = longSha.Substring(0, 16);
+
+ return result;
+ }
+
+ private static uint[] CreateLookup32()
+ {
+ var result = new uint[256];
+ for (var i = 0; i < 256; i++)
+ {
+ var s = i.ToString("X2");
+ result[i] = s[0] + ((uint)s[1] << 16);
+ }
+
+ return result;
+ }
+
+ private static string ByteArrayToHexViaLookup32(byte[] bytes)
+ {
+ var lookup32 = m_lookup32;
+ var result = new char[bytes.Length * 2];
+ for (var i = 0; i < bytes.Length; i++)
+ {
+ var val = lookup32[bytes[i]];
+ result[2 * i] = (char)val;
+ result[2 * i + 1] = (char)(val >> 16);
+ }
+
+ return new string(result);
+ }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs b/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs
new file mode 100644
index 000000000..7b1fd82ec
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs
@@ -0,0 +1,48 @@
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Reflection;
+using TLSharp.Core;
+using TLSharp.Core.Network;
+
+namespace Duplicati.Library.Backend.Extensions
+{
+ public static class TelegramClientExtensions
+ {
+ private static BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Default;
+
+ public static bool IsReallyConnected(this TelegramClient client)
+ {
+ if (client.IsConnected == false)
+ {
+ return false;
+ }
+
+ var senderFieldInfo = typeof(TelegramClient).GetField("sender", _bindingFlags);
+ var sender = (MtProtoSender)senderFieldInfo.GetValue(client);
+
+ if (sender == null)
+ {
+ return false;
+ }
+
+ var transportFieldInfo = typeof(TelegramClient).GetField("transport", _bindingFlags);
+ var transportField = (TcpTransport)transportFieldInfo.GetValue(client);
+
+ if (transportField == null || transportField.IsConnected == false)
+ {
+ return false;
+ }
+
+ var tcpClientFieldInfo = typeof(TcpTransport).GetField("tcpClient", _bindingFlags);
+ var tcpClient = (TcpClient)tcpClientFieldInfo.GetValue(transportField);
+
+ if (tcpClient == null || tcpClient.Connected == false)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..d33f05968
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs
@@ -0,0 +1,53 @@
+#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.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Telegram")]
+[assembly: AssemblyDescription("A Telegram backend for Duplicati")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Duplicati Team")]
+[assembly: AssemblyProduct("Duplicati.Backend.Telegram")]
+[assembly: AssemblyCopyright("LGPL, Copyright © Duplicati Team 2015")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("afa68988-4d82-490e-8044-acbc7d52ef4f")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("2.0.0.7")]
+[assembly: AssemblyFileVersion("2.0.0.7")]
diff --git a/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs
new file mode 100644
index 000000000..d436bdb27
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs
@@ -0,0 +1,48 @@
+using System.IO;
+using Duplicati.Library.Utility;
+
+namespace Duplicati.Library.Backend
+{
+ /// <summary>
+ /// Private helper class to fix a bug with the StreamReader
+ /// </summary>
+ internal class StreamReadHelper : OverrideableStream
+ {
+ /// <summary>
+ /// Once the stream has returned 0 as the read count it is disposed,
+ /// and subsequent read requests will throw an ObjectDisposedException
+ /// </summary>
+ private bool m_empty;
+
+ /// <summary>
+ /// Basic initialization, just pass the stream to the super class
+ /// </summary>
+ /// <param name="stream"></param>
+ public StreamReadHelper(Stream stream) : base(stream)
+ { }
+
+ /// <summary>
+ /// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted
+ /// </summary>
+ /// <param name="buffer">The buffer to place data in</param>
+ /// <param name="offset">The offset into the buffer to start at</param>
+ /// <param name="count">The number of bytes to read</param>
+ /// <returns>The number of bytes read</returns>
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var readCount = 0;
+ int a;
+
+ while (!m_empty && count > 0)
+ {
+ a = base.Read(buffer, offset, count);
+ readCount += a;
+ count -= a;
+ offset += a;
+ m_empty = a == 0;
+ }
+
+ return readCount;
+ }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/Strings.cs b/Duplicati/Library/Backend/Telegram/Strings.cs
new file mode 100644
index 000000000..16a7c79a3
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/Strings.cs
@@ -0,0 +1,63 @@
+using Duplicati.Library.Localization.Short;
+
+namespace Duplicati.Library.Backend
+{
+ internal static class Strings
+ {
+ #region Constants
+
+ public const string API_ID_KEY = "api-id";
+ public const string API_HASH_KEY = "api-hash";
+ public const string PHONE_NUMBER_KEY = "phone-number";
+ public const string AUTH_CODE_KEY = "auth-code";
+ public const string AUTH_PASSWORD = "auth-password";
+ public const string CHANNEL_NAME = "channel-name";
+
+ #endregion
+
+ #region General
+
+ public static string DisplayName { get; } = "Telegram";
+ public static string Description => LC.L("This backend can read and write data to a Telegram backend");
+
+ #endregion
+
+ #region Errors
+
+ public static string WrongAuthCodeError => LC.L("The auth code is incorrect");
+ public static string NoAuthCodeError => LC.L("The auth code is missing");
+ public static string NoPasswordError => LC.L("The password is missing");
+ public static string NoChannelNameError => LC.L("The channel name is missing");
+ public static string NoApiIdError => LC.L("The API ID is missing");
+ public static string NoApiHashError => LC.L("The API hash is missing");
+ public static string NoPhoneNumberError => LC.L("The phone number is missing");
+
+ #endregion
+
+
+ #region Descriptions
+
+ public static string ApiIdShort => LC.L("The API ID");
+ public static string ApiHashShort => LC.L("The API hash");
+ public static string ApiIdLong => LC.L("The API ID retrieved from https://my.telegram.org/");
+ public static string ApiHashLong => LC.L("The API hash retrieved from https://my.telegram.org/");
+ public static string PhoneNumberShort => LC.L("Your phone number");
+ public static string PhoneNumberLong => LC.L("The phone number you registered with");
+ public static string AuthCodeShort => LC.L("The code you should have received (if you did)");
+ public static string AuthCodeLong => LC.L("The auth code that you received. Input only if you did receive it");
+ public static string PasswordShort => LC.L("2FA password (if enabled)");
+ public static string PasswordLong => LC.L("The 2 step verification password. Input only if you have set it up");
+ public static string ChannelName => LC.L("The channel name of the backup");
+
+ #endregion
+
+ #region Formats
+
+ public const string TELEGRAM_FLOOD = "It's required to wait {0} seconds before continuing";
+ public const string STARTING_EXECUTING = "Starting executing action {0}";
+ public const string DONE_EXECUTING = "Done executing action {0}";
+ public const string USER_INFO_EXC = "Exception thrown that should be shown on UI";
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/TelegramBackend.cs b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs
new file mode 100644
index 000000000..373809659
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs
@@ -0,0 +1,521 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Duplicati.Library.Backend.Extensions;
+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
+{
+ public class Telegram : IStreamingBackend
+ {
+ private static TelegramClient m_telegramClient;
+ private readonly EncryptedFileSessionStore m_encSessionStore;
+
+ 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 string m_phoneCodeHash;
+ private static readonly string m_logTag = Log.LogTagFromType(typeof(Telegram));
+ private const int BYTES_IN_MEBIBYTE = 1048576;
+
+ 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));
+ }
+
+ m_encSessionStore = new EncryptedFileSessionStore($"{m_apiHash}_{m_apiId}");
+ InitializeTelegramClient();
+ }
+
+ private void InitializeTelegramClient()
+ {
+ m_telegramClient = m_telegramClient ?? new TelegramClient(m_apiId, m_apiHash, m_encSessionStore, m_phoneNumber);
+ }
+
+ public void Dispose()
+ {
+ // Do not dispose m_telegramClient.
+ // There are bugs connected with reusing
+ // the old sockets
+ }
+
+ public string DisplayName { get; } = Strings.DisplayName;
+ public string ProtocolKey { get; } = "telegram";
+
+ 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, Stream stream, CancellationToken cancelToken)
+ {
+ SafeExecute(() =>
+ {
+ cancelToken.ThrowIfCancellationRequested();
+
+ Authenticate();
+
+ cancelToken.ThrowIfCancellationRequested();
+
+ var channel = GetChannel();
+
+ cancelToken.ThrowIfCancellationRequested();
+
+ using (var sr = new StreamReader(new StreamReadHelper(stream)))
+ {
+ cancelToken.ThrowIfCancellationRequested();
+ EnsureConnected(cancelToken);
+
+ cancelToken.ThrowIfCancellationRequested();
+ var file = m_telegramClient.UploadFile(remotename, sr, cancelToken).GetAwaiter().GetResult();
+
+ cancelToken.ThrowIfCancellationRequested();
+ var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = (long)channel.AccessHash};
+ var fileNameAttribute = new TLDocumentAttributeFilename
+ {
+ FileName = remotename
+ };
+
+ EnsureConnected(cancelToken);
+ m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector<TLAbsDocumentAttribute> {fileNameAttribute}, cancelToken).GetAwaiter().GetResult();
+ }
+ },
+ nameof(PutAsync));
+
+ return Task.CompletedTask;
+ }
+
+ public void Get(string remotename, Stream stream)
+ {
+ SafeExecute(() =>
+ {
+ var fileInfo = ListChannelFileInfos().First(fi => fi.Name == remotename);
+ var fileLocation = fileInfo.ToFileLocation();
+
+ var limit = BYTES_IN_MEBIBYTE;
+ var currentOffset = 0;
+
+
+ while (currentOffset < fileInfo.Size)
+ {
+ try
+ {
+ EnsureConnected();
+ var file = m_telegramClient.GetFile(fileLocation, limit, currentOffset).GetAwaiter().GetResult();
+ stream.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;
+ }
+ }
+ }
+ },
+ $"{nameof(Get)}");
+ }
+
+ public Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
+ {
+ using (var fs = File.OpenRead(filename))
+ {
+ PutAsync(remotename, fs, cancelToken).GetAwaiter().GetResult();
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public void Get(string remotename, string filename)
+ {
+ using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write))
+ {
+ Get(remotename, fs);
+ }
+ }
+
+ 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 oldMinDate = 0L;
+ var newMinDate = (long?)null;
+
+ while (oldMinDate != newMinDate)
+ {
+ oldMinDate = newMinDate ?? 0L;
+ RetrieveMessages(inputPeerChannel, result, oldMinDate);
+ if (result.Any() == false)
+ {
+ break;
+ }
+
+ newMinDate = result.Min(cfi => Utility.Utility.NormalizeDateTimeToEpochSeconds(cfi.Date));
+ }
+
+ result = result.Distinct().ToList();
+ return result;
+ }
+
+ private void RetrieveMessages(TLInputPeerChannel inputPeerChannel, List<ChannelFileInfo> result, long offsetDate)
+ {
+ EnsureConnected();
+ var absHistory = m_telegramClient.GetHistoryAsync(inputPeerChannel, offsetDate: (int)offsetDate).GetAwaiter().GetResult();
+ var history = ((TLChannelMessages)absHistory).Messages.OfType<TLMessage>();
+
+ foreach (var msg in history)
+ {
+ if (msg.Media is TLMessageMediaDocument media &&
+ media.Document is TLDocument mediaDoc)
+ {
+ var fileInfo = new ChannelFileInfo(
+ msg.Id,
+ mediaDoc.AccessHash,
+ mediaDoc.Id,
+ mediaDoc.Version,
+ mediaDoc.Size,
+ media.Caption,
+ DateTimeOffset.FromUnixTimeSeconds(msg.Date).UtcDateTime);
+
+ result.Add(fileInfo);
+ }
+ }
+ }
+
+ 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.ChannelName, Strings.ChannelName)
+ };
+
+ public string Description { get; } = Strings.Description;
+
+ public string[] DNSName { get; }
+
+ public void Test()
+ {
+ SafeExecute(Authenticate, nameof(Authenticate));
+ }
+
+ public void CreateFolder()
+ {
+ SafeExecute(() =>
+ {
+ Authenticate();
+ EnsureChannelCreated();
+ },
+ nameof(CreateFolder));
+ }
+
+ private TLChannel GetChannel()
+ {
+ if (m_channelCache != null)
+ {
+ return m_channelCache;
+ }
+
+ var absChats = GetChats();
+
+ var channel = (TLChannel)absChats.FirstOrDefault(chat => chat is TLChannel tlChannel && tlChannel.Title == m_channelName);
+ m_channelCache = channel;
+ return channel;
+ }
+
+ private IEnumerable<TLAbsChat> GetChats()
+ {
+ var lastDate = 0;
+ while (true)
+ {
+ EnsureConnected();
+ var dialogs = m_telegramClient.GetUserDialogsAsync(lastDate).GetAwaiter().GetResult();
+ var tlDialogs = dialogs as TLDialogs;
+ var tlDialogsSlice = dialogs as TLDialogsSlice;
+
+ foreach (var chat in tlDialogs?.Chats ?? tlDialogsSlice?.Chats)
+ {
+ switch (chat)
+ {
+ case TLChannelForbidden _:
+ case TLChatForbidden _:
+ break;
+ case TLChat c:
+ lastDate = c.Date;
+ break;
+ case TLChannel c:
+ lastDate = c.Date;
+ break;
+
+ default:
+ throw new NotSupportedException($"Unsupported chat type {chat.GetType()}");
+ }
+
+ yield return chat;
+ }
+
+ if (tlDialogs?.Dialogs?.Count < 100 || tlDialogsSlice?.Dialogs?.Count < 100)
+ {
+ yield break;
+ }
+ }
+ }
+
+ 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
+ {
+ if (m_phoneCodeHash == null)
+ {
+ EnsureConnected();
+ var phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult();
+ SetPhoneCodeHash(phoneCodeHash);
+ m_telegramClient.Session.Save();
+
+ if (string.IsNullOrEmpty(m_authCode))
+ {
+ throw new UserInformationException(Strings.NoAuthCodeError, nameof(Strings.NoAuthCodeError));
+ }
+
+ throw new UserInformationException(Strings.WrongAuthCodeError, nameof(Strings.WrongAuthCodeError));
+ }
+
+ m_telegramClient.MakeAuthAsync(m_phoneNumber, m_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(CancellationToken cancelToken = default(CancellationToken))
+ {
+ if (m_telegramClient.IsReallyConnected())
+ {
+ return;
+ }
+
+ cancelToken.ThrowIfCancellationRequested();
+
+ m_telegramClient.ConnectAsync(false, cancelToken).GetAwaiter().GetResult();
+
+ cancelToken.ThrowIfCancellationRequested();
+
+ if (m_telegramClient.IsReallyConnected() == false)
+ {
+ throw new WebException("Unable to connect to telegram");
+ }
+ }
+
+ private bool IsAuthenticated()
+ {
+ var isAuthorized = m_telegramClient.IsUserAuthorized();
+ return isAuthorized;
+ }
+
+ private static void SetPhoneCodeHash(string phoneCodeHash)
+ {
+ m_phoneCodeHash = phoneCodeHash;
+ }
+
+ private void SafeExecute(Action action, string actionName)
+ {
+ lock (m_lockObj)
+ {
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
+ try
+ {
+ action();
+ }
+ catch (UserInformationException uiExc)
+ {
+ Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC);
+ throw;
+ }
+ catch (FloodException floodExc)
+ {
+ var randSeconds = new Random().Next(2, 15);
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
+ Thread.Sleep(floodExc.TimeToWait + TimeSpan.FromSeconds(randSeconds));
+ SafeExecute(action, actionName);
+ }
+ }
+
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
+ }
+
+ private T SafeExecute<T>(Func<T> func, string actionName)
+ {
+ lock (m_lockObj)
+ {
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName);
+ try
+ {
+ var res = func();
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
+ return res;
+ }
+ catch (UserInformationException uiExc)
+ {
+ Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC);
+ throw;
+ }
+ catch (FloodException floodExc)
+ {
+ var randSeconds = new Random().Next(2, 15);
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds);
+ Thread.Sleep(floodExc.TimeToWait);
+ var res = SafeExecute(func, actionName);
+ Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName);
+ return res;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Duplicati/Library/Backend/Telegram/app.config b/Duplicati/Library/Backend/Telegram/app.config
new file mode 100644
index 000000000..ba98fc967
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/app.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <startup>
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1"/>
+ </startup>
+</configuration>
diff --git a/Duplicati/Library/Backend/Telegram/packages.config b/Duplicati/Library/Backend/Telegram/packages.config
new file mode 100644
index 000000000..ecd8f06db
--- /dev/null
+++ b/Duplicati/Library/Backend/Telegram/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="SharpAESCrypt.exe" version="1.3.3" targetFramework="net471" />
+ <package id="TLSharp" version="0.1.0.574" targetFramework="net471" />
+</packages> \ No newline at end of file