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