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 }