1 module telega.botapi; 2 3 import vibe.core.log; 4 import asdf : Asdf, serializedAs; 5 import asdf.serialization : deserialize; 6 import std.typecons : Nullable; 7 import std.exception : enforce; 8 import std.traits : isSomeString, isIntegral; 9 import telega.http : HttpClient; 10 import telega.serialization : serializeToJsonString, JsonableAlgebraicProxy; 11 12 13 enum HTTPMethod 14 { 15 GET, 16 POST 17 } 18 19 @serializedAs!ChatIdProxy 20 struct ChatId 21 { 22 import std.conv; 23 24 string id; 25 26 private bool _isString = true; 27 28 alias id this; 29 30 this(long id) 31 { 32 this.id = id.to!string; 33 _isString = false; 34 } 35 36 this(string id) 37 { 38 this.id = id; 39 } 40 41 void opAssign(long id) 42 { 43 this.id = id.to!string; 44 _isString = false; 45 } 46 47 void opAssign(string id) 48 { 49 this.id = id; 50 _isString = true; 51 } 52 53 @property 54 bool isString() 55 { 56 return _isString; 57 } 58 59 long opCast(T)() 60 if (is(T == long)) 61 { 62 if (_isString) { 63 return 0; 64 } 65 66 return id.to!long; 67 } 68 } 69 70 struct ChatIdProxy 71 { 72 ChatId id; 73 74 this(ChatId id) 75 { 76 this.id = id; 77 } 78 79 ChatId opCast(T : ChatId)() 80 { 81 return id; 82 } 83 84 static ChatIdProxy deserialize(Asdf v) 85 { 86 return ChatIdProxy(ChatId(cast(string)v)); 87 } 88 } 89 90 unittest 91 { 92 ChatId chatId; 93 94 chatId = 45; 95 assert(chatId.isString() == false); 96 97 chatId = "@chat"; 98 assert(chatId.isString() == true); 99 100 string chatIdString = chatId; 101 assert(chatIdString == "@chat"); 102 103 long chatIdNum = cast(long)chatId; 104 assert(chatIdNum == 0); 105 106 chatId = 42; 107 108 chatIdNum = cast(long)chatId; 109 assert(chatIdNum == 42); 110 111 string chatIdFunc(ChatId id) 112 { 113 return id; 114 } 115 116 assert(chatIdFunc(cast(ChatId)"abc") == "abc"); 117 assert(chatIdFunc(cast(ChatId)45) == "45"); 118 } 119 120 121 class TelegramBotApiException : Exception 122 { 123 ushort code; 124 125 this(ushort code, string description, string file = __FILE__, size_t line = __LINE__, 126 Throwable next = null) @nogc @safe pure nothrow 127 { 128 this.code = code; 129 super(description, file, line, next); 130 } 131 } 132 133 enum isTelegramId(T) = isSomeString!T || isIntegral!T || is(T == ChatId); 134 135 mixin template TelegramMethod(string path, HTTPMethod method = HTTPMethod.POST) 136 { 137 import asdf.serialization : serializationIgnore; 138 139 public: 140 @serializationIgnore 141 immutable string _path = path; 142 143 @serializationIgnore 144 immutable HTTPMethod _httpMethod = method; 145 } 146 147 /// UDA for telegram methods 148 struct Method 149 { 150 string path; 151 } 152 153 /******************************************************************/ 154 /* Telegram API */ 155 /******************************************************************/ 156 157 enum BaseApiUrl = "https://api.telegram.org/bot"; 158 159 class BotApi 160 { 161 private: 162 string baseUrl; 163 string apiUrl; 164 165 ulong requestCounter = 1; 166 167 struct MethodResult(T) 168 { 169 bool ok; 170 T result; 171 ushort error_code; 172 string description; 173 } 174 175 protected: 176 HttpClient httpClient; 177 178 public: 179 this(string token, string baseUrl = BaseApiUrl, HttpClient httpClient = null) 180 { 181 this.baseUrl = baseUrl; 182 this.apiUrl = baseUrl ~ token; 183 184 if (httpClient is null) { 185 version(TelegaVibedDriver) { 186 import telega.drivers.vibe; 187 188 httpClient = new VibedHttpClient(); 189 } else version(TelegaRequestsDriver) { 190 import telega.drivers.requests; 191 192 httpClient = new RequestsHttpClient(); 193 } else { 194 assert(false, `No HTTP client is set, maybe you should enable "default" configuration?`); 195 } 196 } 197 this.httpClient = httpClient; 198 } 199 200 T callMethod(T, M)(M method) 201 { 202 T result; 203 204 logDiagnostic("[%d] Requesting %s", requestCounter, method._path); 205 206 version(unittest) 207 { 208 import std.stdio; 209 serializeToJsonString(method).writeln(); 210 } else { 211 string answer; 212 213 if (method._httpMethod == HTTPMethod.POST) { 214 string bodyJson = serializeToJsonString(method); 215 logDebugV("[%d] Sending body:\n %s", requestCounter, bodyJson); 216 217 answer = httpClient.sendPostRequestJson(apiUrl ~ method._path, bodyJson); 218 } else { 219 answer = httpClient.sendGetRequest(apiUrl ~ method._path); 220 } 221 222 logDebugV("[%d] Data received:\n %s", requestCounter, answer); 223 224 auto json = answer.deserialize!(MethodResult!T); 225 226 enforce(json.ok == true, new TelegramBotApiException(json.error_code, json.description)); 227 228 result = json.result; 229 requestCounter++; 230 } 231 232 return result; 233 } 234 }