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 }