mirror of
https://github.com/wiz0u/WTelegramClient.git
synced 2025-12-06 06:52:01 +01:00
UploadFile & SendMessage/Media helper methods
This commit is contained in:
parent
18dac72b20
commit
e53293b8e6
128
Client.cs
128
Client.cs
|
|
@ -9,6 +9,7 @@ using System.Net.Sockets;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TL;
|
using TL;
|
||||||
using static WTelegram.Encryption;
|
using static WTelegram.Encryption;
|
||||||
|
|
@ -62,7 +63,7 @@ namespace WTelegram
|
||||||
_ => null // api_id api_hash phone_number verification_code... it's up to you to reply to these correctly
|
_ => null // api_id api_hash phone_number verification_code... it's up to you to reply to these correctly
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string DefaultConfigOrAsk(string config)
|
public static string DefaultConfigOrAsk(string config)
|
||||||
{
|
{
|
||||||
var value = DefaultConfig(config);
|
var value = DefaultConfig(config);
|
||||||
if (value != null) return value;
|
if (value != null) return value;
|
||||||
|
|
@ -225,8 +226,12 @@ namespace WTelegram
|
||||||
var length = reader.ReadInt32(); // int32 message_data_length
|
var length = reader.ReadInt32(); // int32 message_data_length
|
||||||
|
|
||||||
if (serverSalt != _session.Salt)
|
if (serverSalt != _session.Salt)
|
||||||
|
{
|
||||||
|
Helpers.Log(3, $"Server salt has changed: {_session.Salt:X8} -> {serverSalt:X8}");
|
||||||
|
_session.Salt = serverSalt;
|
||||||
if (++_unexpectedSaltChange >= 10)
|
if (++_unexpectedSaltChange >= 10)
|
||||||
throw new ApplicationException($"Server salt changed unexpectedly more than 10 times during this run");
|
throw new ApplicationException($"Server salt changed unexpectedly more than 10 times during this run");
|
||||||
|
}
|
||||||
if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}");
|
if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}");
|
||||||
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
|
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
|
||||||
if ((seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msgId);
|
if ((seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msgId);
|
||||||
|
|
@ -255,7 +260,8 @@ namespace WTelegram
|
||||||
private async Task<byte[]> RecvFrameAsync()
|
private async Task<byte[]> RecvFrameAsync()
|
||||||
{
|
{
|
||||||
byte[] frame = new byte[8];
|
byte[] frame = new byte[8];
|
||||||
await FullReadAsync(frame, 8);
|
if (await FullReadAsync(_networkStream, frame, 8) != 8)
|
||||||
|
throw new ApplicationException("Could not read frame prefix : Connection shut down");
|
||||||
int length = BitConverter.ToInt32(frame) - 12;
|
int length = BitConverter.ToInt32(frame) - 12;
|
||||||
if (length <= 0 || length >= 0x10000)
|
if (length <= 0 || length >= 0x10000)
|
||||||
throw new ApplicationException("Invalid frame_len");
|
throw new ApplicationException("Invalid frame_len");
|
||||||
|
|
@ -266,23 +272,26 @@ namespace WTelegram
|
||||||
_frame_seqRx = seqno + 1;
|
_frame_seqRx = seqno + 1;
|
||||||
}
|
}
|
||||||
var payload = new byte[length];
|
var payload = new byte[length];
|
||||||
await FullReadAsync(payload, length);
|
if (await FullReadAsync(_networkStream, payload, length) != length)
|
||||||
|
throw new ApplicationException("Could not read frame data : Connection shut down");
|
||||||
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
|
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
|
||||||
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
|
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
|
||||||
await FullReadAsync(frame, 4);
|
if (await FullReadAsync(_networkStream, frame, 4) != 4)
|
||||||
|
throw new ApplicationException("Could not read frame CRC : Connection shut down");
|
||||||
if (crc32 != BitConverter.ToUInt32(frame))
|
if (crc32 != BitConverter.ToUInt32(frame))
|
||||||
throw new ApplicationException("Invalid envelope CRC32");
|
throw new ApplicationException("Invalid envelope CRC32");
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FullReadAsync(byte[] buffer, int length)
|
private static async Task<int> FullReadAsync(Stream stream, byte[] buffer, int length)
|
||||||
{
|
{
|
||||||
for (int offset = 0; offset != length;)
|
for (int offset = 0; offset != length;)
|
||||||
{
|
{
|
||||||
var read = await _networkStream.ReadAsync(buffer.AsMemory(offset, length - offset));
|
var read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset));
|
||||||
if (read == 0) throw new ApplicationException("Connection shut down");
|
if (read == 0) return offset;
|
||||||
offset += read;
|
offset += read;
|
||||||
}
|
}
|
||||||
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RpcResult DeserializeRpcResult(BinaryReader reader)
|
private RpcResult DeserializeRpcResult(BinaryReader reader)
|
||||||
|
|
@ -402,5 +411,110 @@ namespace WTelegram
|
||||||
_session.User = Schema.Serialize(user);
|
_session.User = Schema.Serialize(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region TL-Helpers
|
||||||
|
|
||||||
|
/// <summary>Helper function to upload a file to Telegram</summary>
|
||||||
|
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
|
||||||
|
public Task<InputFileBase> UploadFileAsync(string pathname)
|
||||||
|
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname));
|
||||||
|
|
||||||
|
public async Task<InputFileBase> UploadFileAsync(Stream stream, string filename)
|
||||||
|
{
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
using (stream)
|
||||||
|
{
|
||||||
|
long length = stream.Length;
|
||||||
|
var isBig = length >= 10 * 1024 * 1024;
|
||||||
|
const int partSize = 512 * 1024;
|
||||||
|
int file_total_parts = (int)((length - 1) / partSize) + 1;
|
||||||
|
long file_id = Helpers.RandomLong();
|
||||||
|
var bytes = new byte[Math.Min(partSize, length)];
|
||||||
|
int file_part = 0, read;
|
||||||
|
for (long bytesLeft = length; bytesLeft != 0; file_part++)
|
||||||
|
{
|
||||||
|
// TODO: parallelize several parts sending through a N-semaphore?
|
||||||
|
read = await FullReadAsync(stream, bytes, (int)Math.Min(partSize, bytesLeft));
|
||||||
|
await CallAsync<bool>(isBig
|
||||||
|
? new Upload_SaveBigFilePart { bytes = bytes, file_id = file_id, file_part = file_part, file_total_parts = file_total_parts }
|
||||||
|
: new Upload_SaveFilePart { bytes = bytes, file_id = file_id, file_part = file_part });
|
||||||
|
if (!isBig) md5.TransformBlock(bytes, 0, read, null, 0);
|
||||||
|
bytesLeft -= read;
|
||||||
|
if (read < partSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
|
||||||
|
}
|
||||||
|
if (!isBig) md5.TransformFinalBlock(bytes, 0, 0);
|
||||||
|
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
|
||||||
|
: new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Helper function to send a text or media message more easily</summary>
|
||||||
|
/// <param name="peer">destination of message</param>
|
||||||
|
/// <param name="caption">media caption</param>
|
||||||
|
/// <param name="mediaFile"><see langword="null"/> or a media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
|
||||||
|
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
|
||||||
|
public Task<UpdatesBase> SendMediaAsync(InputPeer peer, string caption, InputFileBase mediaFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
|
||||||
|
{
|
||||||
|
var filename = mediaFile is InputFile iFile ? iFile.name : (mediaFile as InputFileBig)?.name;
|
||||||
|
mimeType ??= Path.GetExtension(filename).ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
|
||||||
|
".gif" => "image/gif",
|
||||||
|
".webp" => "image/webp",
|
||||||
|
".mp4" => "video/mp4",
|
||||||
|
".mp3" => "audio/mpeg",
|
||||||
|
".wav" => "audio/x-wav",
|
||||||
|
_ => "", // send as generic document with undefined MIME type
|
||||||
|
};
|
||||||
|
if (mimeType == "photo")
|
||||||
|
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = mediaFile },
|
||||||
|
reply_to_msg_id, entities, schedule_date, disable_preview);
|
||||||
|
var attributes = filename == null ? Array.Empty<DocumentAttribute>() : new[] { new DocumentAttributeFilename { file_name = filename } };
|
||||||
|
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument
|
||||||
|
{
|
||||||
|
file = mediaFile, mime_type = mimeType, attributes = attributes
|
||||||
|
}, reply_to_msg_id, entities, schedule_date, disable_preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Helper function to send a text or media message</summary>
|
||||||
|
/// <param name="peer">destination of message</param>
|
||||||
|
/// <param name="text">text, or media caption</param>
|
||||||
|
/// <param name="media">media specification or <see langword="null"/></param>
|
||||||
|
public Task<UpdatesBase> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
|
||||||
|
{
|
||||||
|
ITLFunction<UpdatesBase> request = (media == null)
|
||||||
|
? new Messages_SendMessage
|
||||||
|
{
|
||||||
|
flags = GetFlags(),
|
||||||
|
peer = peer,
|
||||||
|
reply_to_msg_id = reply_to_msg_id,
|
||||||
|
message = text,
|
||||||
|
random_id = Helpers.RandomLong(),
|
||||||
|
entities = entities,
|
||||||
|
schedule_date = schedule_date
|
||||||
|
}
|
||||||
|
: new Messages_SendMedia
|
||||||
|
{
|
||||||
|
flags = (Messages_SendMedia.Flags)GetFlags(),
|
||||||
|
peer = peer,
|
||||||
|
reply_to_msg_id = reply_to_msg_id,
|
||||||
|
media = media,
|
||||||
|
message = text,
|
||||||
|
random_id = Helpers.RandomLong(),
|
||||||
|
entities = entities,
|
||||||
|
schedule_date = schedule_date
|
||||||
|
};
|
||||||
|
return CallAsync(request);
|
||||||
|
|
||||||
|
Messages_SendMessage.Flags GetFlags()
|
||||||
|
{
|
||||||
|
return ((reply_to_msg_id != 0) ? Messages_SendMessage.Flags.has_reply_to_msg_id : 0)
|
||||||
|
| (disable_preview ? Messages_SendMessage.Flags.no_webpage : 0)
|
||||||
|
// | (reply_markup != null ? Messages_SendMessage.Flags.has_reply_markup : 0)
|
||||||
|
| (entities != null ? Messages_SendMessage.Flags.has_entities : 0)
|
||||||
|
| (schedule_date != default ? Messages_SendMessage.Flags.has_schedule_date : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ namespace WTelegram
|
||||||
sw.Write(str);
|
sw.Write(str);
|
||||||
lineLen += str.Length;
|
lineLen += str.Length;
|
||||||
}
|
}
|
||||||
sw.WriteLine(" }");
|
sw.WriteLine("}");
|
||||||
}
|
}
|
||||||
foreach (var parm in parms)
|
foreach (var parm in parms)
|
||||||
{
|
{
|
||||||
|
|
@ -213,6 +213,8 @@ namespace WTelegram
|
||||||
else
|
else
|
||||||
return "int";
|
return "int";
|
||||||
}
|
}
|
||||||
|
else if (type == "string")
|
||||||
|
return name.StartsWith("md5") ? "byte[]" : "string";
|
||||||
else
|
else
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ namespace WTelegram
|
||||||
Console.ResetColor();
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long RandomLong()
|
||||||
|
{
|
||||||
|
Span<long> span = stackalloc long[1];
|
||||||
|
System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes(span));
|
||||||
|
return span[0];
|
||||||
|
}
|
||||||
|
|
||||||
public static void LittleEndian(byte[] buffer, int offset, int value)
|
public static void LittleEndian(byte[] buffer, int offset, int value)
|
||||||
{
|
{
|
||||||
buffer[offset + 0] = (byte)value;
|
buffer[offset + 0] = (byte)value;
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ namespace TL
|
||||||
public long id;
|
public long id;
|
||||||
public int parts;
|
public int parts;
|
||||||
public string name;
|
public string name;
|
||||||
public string md5_checksum;
|
public byte[] md5_checksum;
|
||||||
}
|
}
|
||||||
[TLDef(0xFA4F0BB5, "inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile")]
|
[TLDef(0xFA4F0BB5, "inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile")]
|
||||||
public class InputFileBig : InputFileBase
|
public class InputFileBig : InputFileBase
|
||||||
|
|
@ -2033,7 +2033,7 @@ namespace TL
|
||||||
{
|
{
|
||||||
public long id;
|
public long id;
|
||||||
public int parts;
|
public int parts;
|
||||||
public string md5_checksum;
|
public byte[] md5_checksum;
|
||||||
public int key_fingerprint;
|
public int key_fingerprint;
|
||||||
}
|
}
|
||||||
[TLDef(0x5A17B5E5, "inputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile")]
|
[TLDef(0x5A17B5E5, "inputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile")]
|
||||||
|
|
@ -4151,7 +4151,7 @@ namespace TL
|
||||||
{
|
{
|
||||||
public long id;
|
public long id;
|
||||||
public int parts;
|
public int parts;
|
||||||
public string md5_checksum;
|
public byte[] md5_checksum;
|
||||||
public byte[] file_hash;
|
public byte[] file_hash;
|
||||||
public byte[] secret;
|
public byte[] secret;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue