| | | 1 | | using System.Collections; |
| | | 2 | | using System.Globalization; |
| | | 3 | | using System.Text.Json; |
| | | 4 | | using System.Text.Json.Nodes; |
| | | 5 | | using TeleFlow.Telegram.Schema.Types; |
| | | 6 | | |
| | | 7 | | namespace TeleFlow.Telegram.Internal; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Converts a Telegram request payload that contains uploads into multipart fields and files. |
| | | 11 | | /// Nested files are exposed to Telegram JSON fields through attach:// references. |
| | | 12 | | /// This path is used when a user sends local streams/files through generated Bot API methods. |
| | | 13 | | /// </summary> |
| | | 14 | | internal sealed class TelegramMultipartContentBuilder |
| | | 15 | | { |
| | | 16 | | private readonly JsonSerializerOptions _serializerOptions; |
| | 9 | 17 | | private readonly List<TelegramMultipartField> _fields = []; |
| | 9 | 18 | | private readonly List<TelegramMultipartFile> _files = []; |
| | | 19 | | private int _fileIndex; |
| | | 20 | | |
| | | 21 | | public TelegramMultipartContentBuilder(JsonSerializerOptions serializerOptions) |
| | | 22 | | { |
| | 9 | 23 | | ArgumentNullException.ThrowIfNull(serializerOptions); |
| | 9 | 24 | | _serializerOptions = serializerOptions; |
| | 9 | 25 | | } |
| | | 26 | | |
| | | 27 | | public TelegramMultipartTransportContent Build(object payload) |
| | | 28 | | { |
| | 9 | 29 | | ArgumentNullException.ThrowIfNull(payload); |
| | | 30 | | |
| | 153 | 31 | | foreach (var property in TelegramRequestTypeMetadataCache.Get(payload.GetType()).TelegramProperties) |
| | | 32 | | { |
| | 68 | 33 | | var value = property.GetValue(payload); |
| | 68 | 34 | | if (value is null) |
| | | 35 | | { |
| | | 36 | | continue; |
| | | 37 | | } |
| | | 38 | | |
| | 19 | 39 | | var fieldName = property.TelegramName; |
| | 19 | 40 | | if (TelegramRequestUploadDetector.TryGetDirectInputFile(value, out var inputFile)) |
| | | 41 | | { |
| | 7 | 42 | | AddFile(fieldName, inputFile); |
| | 6 | 43 | | continue; |
| | | 44 | | } |
| | | 45 | | |
| | 12 | 46 | | var node = ToJsonNode(value); |
| | 12 | 47 | | if (node is not null) |
| | | 48 | | { |
| | 12 | 49 | | AddField(fieldName, node); |
| | | 50 | | } |
| | | 51 | | } |
| | | 52 | | |
| | 8 | 53 | | return new TelegramMultipartTransportContent(_fields.ToArray(), _files.ToArray()); |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | private JsonNode? ToJsonNode(object? value) |
| | | 57 | | { |
| | 38 | 58 | | if (value is null) |
| | | 59 | | { |
| | 0 | 60 | | return null; |
| | | 61 | | } |
| | | 62 | | |
| | | 63 | | // Nested files are represented in JSON fields by attach:// names. |
| | | 64 | | // The binary stream is added to the same multipart body under that generated name. |
| | 38 | 65 | | if (value is InputFile inputFile) |
| | | 66 | | { |
| | 2 | 67 | | var fileName = "file" + _fileIndex.ToString(CultureInfo.InvariantCulture); |
| | 2 | 68 | | _fileIndex++; |
| | 2 | 69 | | AddFile(fileName, inputFile); |
| | 2 | 70 | | return JsonValue.Create("attach://" + fileName); |
| | | 71 | | } |
| | | 72 | | |
| | 36 | 73 | | if (value is string stringValue) |
| | | 74 | | { |
| | 7 | 75 | | return JsonValue.Create(stringValue); |
| | | 76 | | } |
| | | 77 | | |
| | 29 | 78 | | if (value is bool boolValue) |
| | | 79 | | { |
| | 0 | 80 | | return JsonValue.Create(boolValue); |
| | | 81 | | } |
| | | 82 | | |
| | 29 | 83 | | if (value is byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal) |
| | | 84 | | { |
| | 9 | 85 | | return JsonSerializer.SerializeToNode(value, value.GetType(), _serializerOptions); |
| | | 86 | | } |
| | | 87 | | |
| | 20 | 88 | | if (value is IEnumerable enumerable and not string) |
| | | 89 | | { |
| | 2 | 90 | | var array = new JsonArray(); |
| | 10 | 91 | | foreach (var item in enumerable) |
| | | 92 | | { |
| | 3 | 93 | | array.Add(ToJsonNode(item)); |
| | | 94 | | } |
| | | 95 | | |
| | 2 | 96 | | return array; |
| | | 97 | | } |
| | | 98 | | |
| | 18 | 99 | | var unionCase = TelegramRequestUploadDetector.GetActiveUnionCase(value); |
| | 18 | 100 | | if (unionCase is not null) |
| | | 101 | | { |
| | | 102 | | // Telegram unions serialize as their selected value, not as the wrapper object. |
| | 15 | 103 | | return ToJsonNode(unionCase.Value); |
| | | 104 | | } |
| | | 105 | | |
| | 3 | 106 | | var metadata = TelegramRequestTypeMetadataCache.Get(value.GetType()); |
| | 3 | 107 | | if (metadata.TelegramProperties.Length == 0) |
| | | 108 | | { |
| | 0 | 109 | | return JsonSerializer.SerializeToNode(value, value.GetType(), _serializerOptions); |
| | | 110 | | } |
| | | 111 | | |
| | 3 | 112 | | var jsonObject = new JsonObject(); |
| | 48 | 113 | | foreach (var property in metadata.TelegramProperties) |
| | | 114 | | { |
| | 21 | 115 | | var propertyValue = property.GetValue(value); |
| | 21 | 116 | | if (propertyValue is null) |
| | | 117 | | { |
| | | 118 | | continue; |
| | | 119 | | } |
| | | 120 | | |
| | 8 | 121 | | var node = ToJsonNode(propertyValue); |
| | 8 | 122 | | if (node is not null) |
| | | 123 | | { |
| | 8 | 124 | | jsonObject[property.TelegramName] = node; |
| | | 125 | | } |
| | | 126 | | } |
| | | 127 | | |
| | 3 | 128 | | return jsonObject; |
| | | 129 | | } |
| | | 130 | | |
| | | 131 | | private void AddField(string name, JsonNode node) |
| | | 132 | | { |
| | 12 | 133 | | var value = node switch |
| | 12 | 134 | | { |
| | 11 | 135 | | JsonValue jsonValue when jsonValue.TryGetValue<string>(out var stringValue) => stringValue, |
| | 9 | 136 | | JsonValue jsonValue when jsonValue.TryGetValue<bool>(out var boolValue) => boolValue ? "true" : "false", |
| | 11 | 137 | | _ => node.ToJsonString(_serializerOptions) |
| | 12 | 138 | | }; |
| | | 139 | | |
| | 12 | 140 | | _fields.Add(new TelegramMultipartField(name, value)); |
| | 12 | 141 | | } |
| | | 142 | | |
| | | 143 | | private void AddFile(string name, InputFile inputFile) |
| | | 144 | | { |
| | 9 | 145 | | if (!inputFile.Content.CanRead) |
| | | 146 | | { |
| | 0 | 147 | | throw new InvalidOperationException( |
| | 0 | 148 | | $"InputFile '{inputFile.FileName}' cannot be uploaded because its stream is not readable."); |
| | | 149 | | } |
| | | 150 | | |
| | 9 | 151 | | if (!inputFile.Content.CanSeek) |
| | | 152 | | { |
| | 1 | 153 | | throw new InvalidOperationException( |
| | 1 | 154 | | $"InputFile '{inputFile.FileName}' cannot be uploaded by the default Telegram executor because its strea |
| | | 155 | | } |
| | | 156 | | |
| | 8 | 157 | | inputFile.Content.Position = 0; |
| | 8 | 158 | | _files.Add(new TelegramMultipartFile(name, inputFile.FileName, inputFile.Content)); |
| | 8 | 159 | | } |
| | | 160 | | } |