1 module telega.telegram.basic;
2 
3 import std.typecons : Nullable, nullable;
4 import asdf : Asdf, serializedAs, deserialize;
5 import telega.serialization : JsonableAlgebraicProxy, SerializableEnumProxy, serializeToJsonString;
6 import telega.botapi : BotApi, TelegramMethod, HTTPMethod, ChatId, isTelegramId;
7 import telega.telegram.stickers : Sticker;
8 import telega.telegram.games : Game, Animation, CallbackGame;
9 import telega.telegram.payments : Invoice, SuccessfulPayment, ShippingQuery, PreCheckoutQuery;
10 import telega.telegram.inline : InlineQuery;
11 import telega.telegram.poll : Poll, PollAnswer;
12 
13 
14 version (unittest)
15 {
16     import telega.test : assertEquals;
17 }
18 
19 /******************************************************************/
20 /*                    Telegram types and enums                    */
21 /******************************************************************/
22 
23 struct User
24 {
25     int    id;
26     bool   is_bot;
27     string first_name;
28 
29     Nullable!string last_name;
30     Nullable!string username;
31     Nullable!string language_code;
32 }
33 
34 unittest
35 {
36     string json = `{
37         "id": 42,
38         "is_bot": false,
39         "first_name": "FirstName"
40     }`;
41 
42     User u = deserialize!User(json);
43 
44     u.last_name.isNull
45         .assertEquals(true);
46 }
47 
48 unittest
49 {
50     string json = `{
51         "id": 42,
52         "is_bot": false,
53         "first_name": "FirstName",
54         "last_name": "LastName"
55     }`;
56 
57     User u = deserialize!User(json);
58 
59     u.last_name.isNull
60         .assertEquals(false);
61 }
62 
63 
64 
65 @serializedAs!ChatTypeProxy
66 enum ChatType : string
67 {
68     Private    = "private",
69     Group      = "group",
70     Supergroup = "supergroup",
71     Channel    = "channel"
72 }
73 
74 struct ChatTypeProxy
75 {
76     ChatType t;
77 
78     this(ChatType type)
79     {
80         t = type;
81     }
82 
83     ChatType opCast(T : ChatType)()
84     {
85         return t;
86     }
87 
88     static ChatTypeProxy deserialize(Asdf v)
89     {
90         return ChatTypeProxy(cast(ChatType)cast(string)v);
91     }
92 }
93 
94 struct Chat
95 {
96     long id;
97     ChatType type;
98     Nullable!string title;
99     Nullable!string first_name;
100     Nullable!string last_name;
101     Nullable!string username;
102     Nullable!bool all_members_are_administrators;
103     Nullable!ChatPhoto photo;
104     Nullable!string description;
105     Nullable!string invite_link;
106     // TODO Nullable!Message pinned_message;
107     Nullable!string sticker_set_name;
108     Nullable!bool can_set_sticker_set;
109 }
110 
111 unittest
112 {
113     string json = `{
114         "id": 42,
115         "type": "group",
116         "title": "chat title",
117         "all_members_are_administrators": false
118     }`;
119 
120     Chat c = deserialize!Chat(json);
121 
122     c.id
123         .assertEquals(42);
124     c.type
125         .assertEquals(ChatType.Group);
126 }
127 
128 struct Message
129 {
130     uint                 message_id;
131     uint                 date;
132     Chat                 chat;
133     Nullable!User        from;
134     Nullable!User        forward_from;
135     Nullable!Chat        forward_from_chat;
136     Nullable!uint        forward_from_message_id;
137     Nullable!string      forward_signature;
138     Nullable!string      forward_sender_name;
139     Nullable!uint        forward_date;
140     Nullable!uint        edit_date;
141     Nullable!string      media_group_id;
142     Nullable!string      author_signature;
143     Nullable!string      text;
144     Nullable!(MessageEntity[]) entities;
145     Nullable!(MessageEntity[]) caption_entities;
146     Nullable!Audio             audio;
147     Nullable!Document          document;
148     Nullable!Animation         animation;
149     Nullable!Game              game;
150     Nullable!Poll              poll;
151     Nullable!(PhotoSize[])     photo;
152     Nullable!Sticker           sticker;
153     Nullable!Video             video;
154     Nullable!Voice             voice;
155     Nullable!VideoNote         video_note;
156     // TODO Nullable!Message   reply_to_message;
157     // TODO Nullable!Message   pinned_message;
158     Nullable!string            caption;
159     Nullable!Contact           contact;
160     Nullable!Location          location;
161     Nullable!Venue             venue;
162     Nullable!(User[])          new_chat_members;
163     Nullable!User              left_chat_member;
164     Nullable!string            new_chat_title;
165     Nullable!(PhotoSize[])     new_chat_photo;
166     Nullable!bool              delete_chat_photo;
167     Nullable!bool              group_chat_created;
168     Nullable!bool              supergroup_chat_created;
169     Nullable!bool              channel_chat_created;
170     Nullable!long              migrate_to_chat_id;
171     Nullable!long              migrate_from_chat_id;
172     Nullable!Invoice           invoice;
173     Nullable!SuccessfulPayment successful_payment;
174     Nullable!string            connected_website;
175 
176     @property
177     uint id()
178     {
179         return message_id;
180     }
181 }
182 
183 unittest
184 {
185     string json = `{
186         "message_id": 1,
187         "date": 2,
188         "chat": {
189             "id": 11,
190             "type": "private"
191         },
192         "photo": [],
193         "new_chat_photo": [
194             {
195                 "file_id": "fid",
196                 "width" : 3,
197                 "height": 4,
198                 "file_size": 450
199             }
200         ]
201     }`;
202 
203     auto m = deserialize!Message(json);
204     assertEquals(m.message_id, 1);
205     assertEquals(m.chat.id, 11);
206 
207     assertEquals(true, m.from.isNull);
208     assertEquals(false, m.photo.isNull);
209     assertEquals(true,  m.photo.get is null);
210 
211     assertEquals(false, m.new_chat_photo.isNull);
212     assertEquals(false, m.new_chat_photo.get is null);
213     assertEquals(1,     m.new_chat_photo.get.length);
214     assertEquals("fid", m.new_chat_photo.get[0].file_id);
215     assertEquals(false, m.new_chat_photo.get[0].file_size.isNull);
216     assertEquals(450,   m.new_chat_photo.get[0].file_size.get);
217 }
218 
219 struct Update
220 {
221     uint             update_id;
222     Nullable!Message message;
223 
224     Nullable!Message edited_message;
225     Nullable!Message channel_post;
226     Nullable!Message edited_channel_post;
227     Nullable!InlineQuery        inline_query;
228     Nullable!ChosenInlineResult chosen_inline_result;
229     Nullable!CallbackQuery      callback_query;
230     Nullable!ShippingQuery      shipping_query;
231     Nullable!PreCheckoutQuery   pre_checkout_query;
232     Nullable!Poll               poll;
233     Nullable!PollAnswer         poll_answer;
234 
235     @property @safe @nogc nothrow pure
236     uint id()
237     {
238         return update_id;
239     }
240 }
241 
242 unittest
243 {
244     string json = `{
245         "update_id": 143,
246         "message": {
247             "message_id": 243,
248             "text": "message text"
249         }
250     }`;
251 
252     Update u = deserialize!Update(json);
253 
254     u.id
255         .assertEquals(143);
256     u.message.message_id
257         .assertEquals(243);
258     u.message.text.get
259         .assertEquals("message text");
260 }
261 
262 enum ParseMode : string
263 {
264     Markdown = "Markdown",
265     HTML     = "HTML",
266     None     = "",
267 }
268 
269 enum MessageEntityType : string
270 {
271     Mention = "mention",
272     Hashtag = "hashtag",
273     Cashtag = "cashtag",
274     BotCommand = "bot_command",
275     Url = "url",
276     Email = "email",
277     PhoneNumber = "phone_number",
278     Bold = "bold",
279     Italic = "italic",
280     Underline = "underline",
281     Strikethrough = "strikethrough",
282     Code = "code",
283     Pre = "pre",
284     TextLink = "text_link",
285     TextMension = "text_mention"
286 }
287 
288 struct MessageEntity
289 {
290     MessageEntityType        type;
291     uint          offset;
292     uint          length;
293     Nullable!string  url;
294     Nullable!User    user;
295 }
296 
297 struct PhotoSize
298 {
299     string        file_id;
300     int           width;
301     int           height;
302 
303     Nullable!uint file_size;
304 }
305 
306 struct Audio
307 {
308     string file_id;
309     uint   duration;
310     Nullable!string performer;
311     Nullable!string title;
312     Nullable!string mime_type;
313     Nullable!uint   file_size;
314     Nullable!PhotoSize thumb;
315 }
316 
317 // TODO Add Nullable fields
318 struct Document
319 {
320     string    file_id;
321     PhotoSize thumb;
322     string    file_name;
323     string    mime_type;
324     uint      file_size;
325 }
326 
327 // TODO Add Nullable fields
328 struct Video
329 {
330     string file_id;
331     uint width;
332     uint height;
333     uint duration;
334     PhotoSize thumb;
335     string mime_type;
336     uint file_size;
337 }
338 
339 // TODO Add Nullable fields
340 struct Voice
341 {
342     string file_id;
343     uint   duration;
344     string mime_type;
345     uint   file_size;
346 }
347 
348 // TODO Add Nullable fields
349 struct VideoNote
350 {
351     string    file_id;
352     uint      length;
353     uint      duration;
354     PhotoSize thumb;
355     uint      file_size;
356 }
357 
358 struct Contact
359 {
360     string phone_number;
361     string first_name;
362     Nullable!string last_name;
363     Nullable!uint user_id;
364     Nullable!string vcard;
365 }
366 
367 unittest
368 {
369     string json = `{
370         "phone_number": "+123456789",
371         "first_name": "FirstName",
372         "last_name": "LstName",
373         "user_id": 42
374     }`;
375 
376     Contact c = deserialize!Contact(json);
377 
378     c.phone_number.assertEquals("+123456789");
379     c.user_id.assertEquals(42);
380 }
381 
382 // TODO Add Nullable fields
383 struct Location
384 {
385     float longitude;
386     float latitude;
387 }
388 
389 struct Venue
390 {
391     Location location;
392     string   title;
393     string   address;
394     Nullable!string   foursquare_id;
395     Nullable!string   foursquare_type;
396 }
397 
398 // TODO Add Nullable fields
399 struct UserProfilePhotos
400 {
401     uint          total_count;
402     PhotoSize[][] photos;
403 }
404 
405 // TODO Add Nullable fields
406 struct File
407 {
408     string file_id;
409     uint   file_size;
410     string file_path;
411 }
412 
413 
414 import std.meta : AliasSeq, staticIndexOf;
415 import std.variant : Algebraic;
416 
417 alias ReplyMarkupStructs = AliasSeq!(
418     ReplyKeyboardMarkup,
419     ReplyKeyboardRemove,
420     InlineKeyboardMarkup,
421     ForceReply
422     );
423 
424 /**
425  Abstract structure for unioining ReplyKeyboardMarkup, ReplyKeyboardRemove,
426  InlineKeyboardMarkup and ForceReply
427 */
428 
429 alias ReplyMarkup = JsonableAlgebraicProxy!ReplyMarkupStructs;
430 enum isReplyMarkup(T) =
431     is(T == ReplyMarkup) || staticIndexOf!(T, ReplyMarkupStructs) >= 0;
432 
433 import std.algorithm.iteration;
434 import std.array;
435 
436 struct ReplyKeyboardMarkup
437 {
438     KeyboardButton[][] keyboard;
439 
440     Nullable!bool      resize_keyboard;
441     Nullable!bool      one_time_keyboard;
442     Nullable!bool      selective;
443 
444     this (string[][] keyboard)
445     {
446         this.keyboard = keyboard.map!toKeyboardButtonRow.array;
447     }
448 
449     void opOpAssign(string op : "~")(KeyboardButton[] buttons)
450     {
451         keyboard ~= buttons;
452     }
453 }
454 
455 struct KeyboardButton
456 {
457     string text;
458 
459     Nullable!bool   request_contact;
460     Nullable!bool   request_location;
461 
462     this(string text)
463     {
464         this.text = text;
465     }
466 
467     this(string text, bool requestContact)
468     {
469         this(text);
470         this.request_contact = requestContact;
471     }
472 
473     this(string text, bool requestContact, bool requestLocation)
474     {
475         this(text, requestContact);
476         this.request_location = requestLocation;
477     }
478 
479     typeof(this) requestContact(bool value = true)
480     {
481         request_contact = value;
482 
483         return this;
484     }
485 
486     typeof(this) requestLocation(bool value = true)
487     {
488         request_location = value;
489 
490         return this;
491     }
492 }
493 
494 KeyboardButton[] toKeyboardButtonRow(string[] row)
495 {
496     return row.map!(b => KeyboardButton(b)).array;
497 }
498 
499 struct ReplyKeyboardRemove
500 {
501     bool remove_keyboard = true;
502     Nullable!bool           selective;
503 }
504 
505 struct InlineKeyboardMarkup
506 {
507     InlineKeyboardButton[][] inline_keyboard;
508 }
509 
510 struct InlineKeyboardButton
511 {
512     string       text;
513     Nullable!string       url;
514     Nullable!string       callback_data;
515     Nullable!string       switch_inline_query;
516     Nullable!string       switch_inline_query_current_chat;
517     Nullable!CallbackGame callback_game;
518     Nullable!bool         pay;
519 }
520 
521 // TODO Add Nullable fields
522 struct CallbackQuery
523 {
524     string           id;
525     User             from;
526     Nullable!Message message;
527     string           inline_message_id;
528     string           chat_instance;
529     string           data;
530     string           game_short_name;
531 }
532 
533 struct ForceReply
534 {
535     bool     force_reply = true;
536     Nullable!bool     selective;
537 }
538 
539 struct ChatPhoto
540 {
541     string small_file_id;
542     string big_file_id;
543 }
544 
545 // TODO Add Nullable fields
546 struct ResponseParameters
547 {
548     long migrate_to_chat_id;
549     uint retry_after;
550 }
551 
552 alias InputMediaStructs = AliasSeq!(
553     InputMediaPhoto,
554     InputMediaVideo,
555     InputMediaAnimation,
556     InputMediaAudio,
557     InputMediaDocument
558 );
559 
560 alias InputMedia = JsonableAlgebraicProxy!InputMediaStructs;
561 
562 struct InputMediaPhoto
563 {
564     string type;
565     string media;
566     Nullable!string caption;
567     Nullable!ParseMode parse_mode;
568 }
569 
570 struct InputMediaVideo
571 {
572     string type;
573     string media;
574     Nullable!string    caption;
575     Nullable!ParseMode parse_mode;
576     Nullable!uint   width;
577     Nullable!uint   height;
578     Nullable!uint   duration;
579     Nullable!bool   supports_streaming;
580 }
581 
582 struct InputMediaAnimation
583 {
584     string type = "animation";
585     string media;
586     Nullable!string thumb; // TODO InputFile
587     Nullable!string caption;
588     Nullable!ParseMode parse_mode;
589     Nullable!uint width;
590     Nullable!uint height;
591     Nullable!uint duration;
592 }
593 
594 struct InputMediaAudio
595 {
596     string type = "audio";
597     string media;
598     Nullable!string thumb; // TODO InputFile
599     Nullable!string caption;
600     Nullable!ParseMode parse_mode;
601     Nullable!uint duration;
602     Nullable!string performer;
603     Nullable!string title;
604 }
605 
606 struct InputMediaDocument
607 {
608     string type = "document";
609     string media;
610     Nullable!string thumb; // TODO InputFile
611     Nullable!string caption;
612     Nullable!ParseMode parse_mode;
613 }
614 
615 struct InputFile
616 {
617     // no fields
618 }
619 
620 alias InputMessageContentStructs = AliasSeq!(
621     InputTextMessageContent, InputLocationMessageContent, InputVenueMessageContent, InputContactMessageContent
622 );
623 
624 alias InputMessageContent = JsonableAlgebraicProxy!InputMessageContentStructs;
625 
626 struct InputTextMessageContent
627 {
628     string message_text;
629     Nullable!ParseMode parse_mode;
630     Nullable!bool   disable_web_page_preview;
631 }
632 
633 struct InputLocationMessageContent
634 {
635     float latitude;
636     float longitude;
637     Nullable!uint  live_period;
638 }
639 
640 /// outgoing
641 struct InputVenueMessageContent
642 {
643     float  latitude;
644     float  longitude;
645     string title;
646     string address;
647     Nullable!string foursquare_id;
648     Nullable!string foursquare_type;
649 }
650 
651 unittest
652 {
653     InputVenueMessageContent ivmc = {
654         latitude : 0.01,
655         longitude : 0.02,
656         title : "t",
657         address : "a",
658         foursquare_id : "fid",
659         foursquare_type : "ft"
660     };
661 
662     ivmc.serializeToJsonString()
663         .assertEquals(
664         `{"latitude":0.01,"longitude":0.02,"title":"t","address":"a","foursquare_id":"fid","foursquare_type":"ft"}`
665         );
666 }
667 
668 struct InputContactMessageContent
669 {
670     string phone_number;
671     string first_name;
672     Nullable!string last_name;
673     Nullable!string vcard;
674 }
675 
676 struct ChosenInlineResult
677 {
678     string   result_id;
679     User     from;
680     Nullable!Location location;
681     Nullable!string   inline_message_id;
682     string   query;
683 }
684 
685 /******************************************************************/
686 /*                        Telegram methods                        */
687 /******************************************************************/
688 
689 @serializedAs!(SerializableEnumProxy!UpdateType)
690 enum UpdateType: string
691 {
692     Message = "message",
693     EditedMessage = "edited_message",
694     ChannelPost = "channel_post",
695     EditedChannelPost = "edited_channel_post",
696     InlineQuery = "inline_query",
697     ChosenInlineResult = "chosen_inline_result",
698     CallbackQuery = "callback_query",
699     ShippingQuery = "shipping_query",
700     PreCheckoutQuery = "pre_checkout_query",
701     Poll = "poll",
702     PollAnswer = "poll_answer"
703 }
704 
705 struct GetUpdatesMethod
706 {
707     enum ubyte DEFAULT_LIMIT = 5;
708     enum uint DEFAULT_TIMEOUT = 30;
709 
710     mixin TelegramMethod!"/getUpdates";
711 
712     Nullable!int   offset;
713     Nullable!ubyte limit;
714     Nullable!uint  timeout;
715     Nullable!(UpdateType[]) allowed_updates;
716 
717     void updateOffset(uint updateId)
718     {
719         import std.algorithm.comparison : max;
720 
721         if (offset.isNull) {
722             offset = updateId + 1;
723         } else {
724             offset = max(offset.get, updateId) + 1;
725         }
726     }
727 }
728 
729 unittest
730 {
731     GetUpdatesMethod m = {
732         offset: 1,
733         allowed_updates: [UpdateType.EditedMessage]
734     };
735 
736     m.serializeToJsonString()
737         .assertEquals(`{"offset":1,"allowed_updates":["edited_message"]}`);
738 }
739 
740 struct GetMeMethod
741 {
742     mixin TelegramMethod!("/getMe", HTTPMethod.GET);
743 }
744 
745 struct SendMessageMethod
746 {
747     mixin TelegramMethod!"/sendMessage";
748 
749     ChatId    chat_id;
750     string    text;
751     Nullable!ParseMode parse_mode;
752     Nullable!bool      disable_web_page_preview;
753     Nullable!bool      disable_notification;
754     Nullable!uint      reply_to_message_id;
755 
756     ReplyMarkup reply_markup;
757 }
758 
759 unittest
760 {
761     SendMessageMethod m = {
762         chat_id: 111,
763         text: "Message text"
764     };
765 
766     m.serializeToJsonString()
767         .assertEquals(`{"chat_id":"111","text":"Message text"}`);
768 }
769 
770 struct ForwardMessageMethod
771 {
772     mixin TelegramMethod!"/forwardMessage";
773 
774     ChatId chat_id;
775     string from_chat_id;
776     Nullable!bool   disable_notification;
777     uint   message_id;
778 }
779 
780 struct SendPhotoMethod
781 {
782     mixin TelegramMethod!"/sendPhoto";
783 
784     ChatId      chat_id;
785     string      photo;
786     Nullable!string      caption;
787     Nullable!ParseMode   parse_mode;
788     Nullable!bool        disable_notification;
789     Nullable!uint        reply_to_message_id;
790     Nullable!ReplyMarkup reply_markup;
791 }
792 
793 unittest
794 {
795     SendPhotoMethod m = {
796         chat_id: "111",
797         photo: "Photo url"
798     };
799 
800     import std.stdio;
801 
802     m.serializeToJsonString().writeln;
803 
804     m.serializeToJsonString()
805         .assertEquals(`{"chat_id":"111","photo":"Photo url"}`);
806 }
807 unittest
808 {
809     SendPhotoMethod m = {
810         chat_id: "111",
811         photo: "Photo url",
812         disable_notification: false,
813         reply_to_message_id: 0
814     };
815 
816     m.serializeToJsonString()
817         .assertEquals(`{"chat_id":"111","photo":"Photo url","disable_notification":false,"reply_to_message_id":0}`);
818 }
819 
820 struct SendAudioMethod
821 {
822     mixin TelegramMethod!"/sendAudio";
823 
824     ChatId      chat_id;
825     string      audio;
826     Nullable!string      caption;
827     Nullable!ParseMode   parse_mode;
828     Nullable!uint        duration;
829     Nullable!string      performer;
830     Nullable!string      title;
831     Nullable!bool        disable_notification;
832     Nullable!uint        reply_to_message_id;
833     Nullable!ReplyMarkup reply_markup;
834 
835 }
836 
837 unittest
838 {
839     SendAudioMethod m = {
840         chat_id: "111",
841         audio: "data"
842     };
843 
844     m.serializeToJsonString()
845         .assertEquals(`{"chat_id":"111","audio":"data"}`);
846 }
847 
848 unittest
849 {
850     SendAudioMethod m = {
851         chat_id: "111",
852         audio: "data",
853         duration: 0,
854         disable_notification:false,
855         reply_to_message_id: 0,
856     };
857 
858     m.serializeToJsonString()
859         .assertEquals(
860             `{"chat_id":"111","audio":"data","duration":0,"disable_notification":false,"reply_to_message_id":0}`
861         );
862 }
863 
864 struct SendDocumentMethod
865 {
866     mixin TelegramMethod!"/sendDocument";
867 
868     ChatId      chat_id;
869     string      document;
870     Nullable!string      caption;
871     Nullable!ParseMode   parse_mode;
872     Nullable!bool        disable_notification;
873     Nullable!uint        reply_to_message_id;
874     Nullable!ReplyMarkup reply_markup;
875 }
876 
877 unittest
878 {
879     SendDocumentMethod m = {
880         chat_id: "111",
881         document: "data"
882     };
883 
884     m.serializeToJsonString()
885         .assertEquals(`{"chat_id":"111","document":"data"}`);
886 }
887 
888 unittest
889 {
890     SendDocumentMethod m = {
891         chat_id: "111",
892         document: "data",
893         disable_notification: false,
894         reply_to_message_id: 0,
895     };
896 
897     m.serializeToJsonString()
898         .assertEquals(`{"chat_id":"111","document":"data","disable_notification":false,"reply_to_message_id":0}`);
899 }
900 
901 struct SendVideoMethod
902 {
903     mixin TelegramMethod!"/sendVideo";
904 
905     string      chat_id;
906     string      video;
907     Nullable!uint        duration;
908     Nullable!uint        width;
909     Nullable!uint        height;
910     Nullable!string      caption;
911     Nullable!ParseMode   parse_mode;
912     Nullable!bool        supports_streaming;
913     Nullable!bool        disable_notification;
914     Nullable!uint        reply_to_message_id;
915     Nullable!ReplyMarkup reply_markup;
916 }
917 
918 unittest
919 {
920     SendVideoMethod m = {
921         chat_id: "111",
922         video: "data"
923     };
924 
925     m.serializeToJsonString()
926         .assertEquals(`{"chat_id":"111","video":"data"}`);
927 }
928 
929 unittest
930 {
931     SendVideoMethod m = {
932         chat_id: "111",
933         video: "data",
934         duration: 0,
935         width: 0,
936         height: 0,
937         supports_streaming: false,
938         disable_notification: false,
939         reply_to_message_id: 0
940     };
941 
942     m.serializeToJsonString()
943         .assertEquals(
944             `{"chat_id":"111","video":"data","duration":0,"width":0,"height":0,"supports_streaming":false,` ~
945             `"disable_notification":false,"reply_to_message_id":0}`
946         );
947 }
948 
949 struct SendVoiceMethod
950 {
951     mixin TelegramMethod!"/sendVoice";
952 
953     ChatId      chat_id;
954     string      voice;
955     Nullable!string      caption;
956     Nullable!ParseMode   parse_mode;
957     Nullable!uint        duration;
958     Nullable!bool        disable_notification;
959     Nullable!uint        reply_to_message_id;
960     Nullable!ReplyMarkup reply_markup;
961 }
962 
963 unittest
964 {
965     SendVoiceMethod m = {
966         chat_id: "111",
967         voice: "data"
968     };
969 
970     m.serializeToJsonString()
971         .assertEquals(`{"chat_id":"111","voice":"data"}`);
972 }
973 
974 unittest
975 {
976     SendVoiceMethod m = {
977         chat_id: "111",
978         voice: "data",
979         duration: 0,
980         disable_notification: false,
981         reply_to_message_id: 0,
982     };
983 
984     m.serializeToJsonString()
985         .assertEquals(
986             `{"chat_id":"111","voice":"data","duration":0,"disable_notification":false,"reply_to_message_id":0}`
987         );
988 }
989 
990 struct SendVideoNoteMethod
991 {
992     mixin TelegramMethod!"/sendVideoNote";
993 
994     ChatId      chat_id;
995     string      video_note;
996     Nullable!uint        duration;
997     Nullable!uint        length;
998     Nullable!bool        disable_notification;
999     Nullable!uint        reply_to_message_id;
1000     Nullable!ReplyMarkup reply_markup;
1001 }
1002 
1003 unittest
1004 {
1005     SendVideoNoteMethod m = {
1006         chat_id: "111",
1007         video_note: "data"
1008     };
1009 
1010     m.serializeToJsonString()
1011         .assertEquals(`{"chat_id":"111","video_note":"data"}`);
1012 }
1013 
1014 struct SendMediaGroupMethod
1015 {
1016     mixin TelegramMethod!"/sendMediaGroup";
1017 
1018     ChatId       chat_id;
1019     InputMedia[] media;
1020     Nullable!bool         disable_notification;
1021     Nullable!uint         reply_to_message_id;
1022 }
1023 
1024 unittest
1025 {
1026     InputMedia im;
1027 
1028     InputMediaPhoto imp = {
1029         type: "t",
1030         media: "m"
1031     };
1032 
1033     im = imp;
1034 
1035     SendMediaGroupMethod m = {
1036         chat_id: "111",
1037         media: [im],
1038     };
1039 
1040     m.serializeToJsonString()
1041         .assertEquals(`{"chat_id":"111","media":[{"type":"t","media":"m"}]}`);
1042 }
1043 
1044 struct SendLocationMethod
1045 {
1046     mixin TelegramMethod!"/sendLocation";
1047 
1048     ChatId      chat_id;
1049     float       latitude;
1050     float       longitude;
1051     Nullable!uint        live_period;
1052     Nullable!bool        disable_notification;
1053     Nullable!uint        reply_to_message_id;
1054     Nullable!ReplyMarkup reply_markup;
1055 }
1056 
1057 unittest
1058 {
1059     SendLocationMethod m = {
1060         chat_id: "111",
1061         latitude: 0.01,
1062         longitude: 0.02
1063     };
1064 
1065     m.serializeToJsonString()
1066         .assertEquals(`{"chat_id":"111","latitude":0.01,"longitude":0.02}`);
1067 }
1068 
1069 struct EditMessageLiveLocationMethod
1070 {
1071     mixin TelegramMethod!"/editMessageLiveLocation";
1072 
1073     ChatId      chat_id;
1074     uint        message_id;
1075     string      inline_message_id;
1076     float       latitude;
1077     float       longitude;
1078     ReplyMarkup reply_markup;
1079 }
1080 
1081 struct StopMessageLiveLocationMethod
1082 {
1083     mixin TelegramMethod!"/stopMessageLiveLocation";
1084 
1085     ChatId      chat_id;
1086     uint        message_id;
1087     string      inline_message_id;
1088     ReplyMarkup reply_markup;
1089 }
1090 
1091 struct SendVenueMethod
1092 {
1093     mixin TelegramMethod!"/sendVenue";
1094 
1095     ChatId      chat_id;
1096     float       latitude;
1097     float       longitude;
1098     string      title;
1099     string      address;
1100     Nullable!string      foursquare_id;
1101     Nullable!string     foursquare_type;
1102     Nullable!bool        disable_notification;
1103     Nullable!uint        reply_to_message_id;
1104     Nullable!ReplyMarkup reply_markup;
1105 }
1106 
1107 unittest
1108 {
1109     SendVenueMethod m = {
1110         chat_id: "111",
1111         latitude: 0.01,
1112         longitude: 0.02,
1113         title: "t",
1114         address: "a"
1115     };
1116 
1117     m.serializeToJsonString()
1118         .assertEquals(`{"chat_id":"111","latitude":0.01,"longitude":0.02,"title":"t","address":"a"}`);
1119 }
1120 
1121 struct SendContactMethod
1122 {
1123     mixin TelegramMethod!"/sendContact";
1124 
1125     ChatId      chat_id;
1126     string      phone_number;
1127     string      first_name;
1128     Nullable!string      last_name;
1129     Nullable!string      vcard;
1130     Nullable!bool        disable_notification;
1131     Nullable!uint        reply_to_message_id;
1132     Nullable!ReplyMarkup reply_markup;
1133 }
1134 
1135 unittest
1136 {
1137     SendContactMethod m = {
1138         chat_id: "111",
1139         phone_number: "+7123",
1140         first_name: "fn"
1141     };
1142 
1143     m.serializeToJsonString()
1144         .assertEquals(`{"chat_id":"111","phone_number":"+7123","first_name":"fn"}`);
1145 }
1146 
1147 enum ChatAction : string
1148 {
1149     Typing = "typing",
1150     UploadPhoto = "upload_photo",
1151     RecordVideo = "record_video",
1152     UploadVideo = "upload_video",
1153     RecordAudio = "record_audio",
1154     UploadAudio = "upload_audio",
1155     UploadDocument = "upload_document",
1156     FindLocation = "find_location",
1157     RecordVideoNote = "record_video_note",
1158     UploadVideoNote = "upload_video_note"
1159 }
1160 
1161 struct SendChatActionMethod
1162 {
1163     mixin TelegramMethod!"/sendChatAction";
1164 
1165     ChatId chat_id;
1166     string action;
1167 }
1168 
1169 struct GetUserProfilePhotosMethod
1170 {
1171     mixin TelegramMethod!("/getUserProfilePhotos", HTTPMethod.GET);
1172 
1173     int  user_id;
1174     uint offset;
1175     uint limit;
1176 }
1177 
1178 struct GetFileMethod
1179 {
1180     mixin TelegramMethod!("/getFile", HTTPMethod.GET);
1181 
1182     string file_id;
1183 }
1184 
1185 struct GetChatMethod
1186 {
1187     mixin TelegramMethod!("/getChat", HTTPMethod.GET);
1188 
1189     ChatId chat_id;
1190 }
1191 
1192 struct AnswerCallbackQueryMethod
1193 {
1194     mixin TelegramMethod!"/answerCallbackQuery";
1195 
1196     string callback_query_id;
1197     string text;
1198     bool   show_alert;
1199     string url;
1200     uint   cache_time;
1201 }
1202 
1203 
1204 // API methods
1205 
1206 Update[] getUpdates(BotApi api, ref GetUpdatesMethod m)
1207 {
1208     return api.callMethod!(Update[])(m);
1209 }
1210 
1211 Update[] getUpdates(BotApi api, int offset, ubyte limit = 5, uint timeout = 30, UpdateType[] allowedUpdates = [])
1212 {
1213     GetUpdatesMethod m = {
1214         offset:  offset,
1215         limit:   limit,
1216         timeout: timeout,
1217         allowed_updates: allowedUpdates.nullable
1218     };
1219 
1220     return api.getUpdates(m);
1221 }
1222 
1223 User getMe(BotApi api)
1224 {
1225     GetMeMethod m;
1226 
1227     return api.callMethod!(User, GetMeMethod)(m);
1228 }
1229 
1230 Message sendMessage(BotApi api, ref SendMessageMethod m)
1231 {
1232     return api.callMethod!(Message, SendMessageMethod)(m);
1233 }
1234 
1235 Message sendMessage(T)(BotApi api, T chatId, string text)
1236     if (isTelegramId!T)
1237 {
1238     SendMessageMethod m = {
1239         chat_id    : chatId,
1240         text       : text,
1241     };
1242 
1243     return sendMessage(api, m);
1244 }
1245 
1246 Message forwardMessage(T1, T2)(BotApi api, T1 chatId, T2 fromChatId, uint messageId)
1247     if (isTelegramId!T1 && isTelegramId!T2)
1248 {
1249     ForwardMessageMethod m = {
1250         message_id : messageId,
1251         chat_id : chatId,
1252         from_chat_id: fromChatId,
1253     };
1254 
1255     return api.callMethod!(Message, ForwardMessageMethod)(m);
1256 }
1257 
1258 Message forwardMessage(BotApi api, ref ForwardMessageMethod m)
1259 {
1260     return api.callMethod!(Message, ForwardMessageMethod)(m);
1261 }
1262 
1263 Message sendPhoto(BotApi api, ref SendPhotoMethod m)
1264 {
1265     return api.callMethod!(Message, SendPhotoMethod)(m);
1266 }
1267 
1268 Message sendPhoto(T1)(BotApi api, T1 chatId, string photo)
1269     if (isTelegramId!T1)
1270 {
1271     SendPhotoMethod m = {
1272         chat_id : chatId,
1273         photo : photo,
1274     };
1275 
1276     return sendPhoto(api, m);
1277 }
1278 
1279 Message sendAudio(BotApi api, ref SendAudioMethod m)
1280 {
1281     return api.callMethod!(Message, SendAudioMethod)(m);
1282 }
1283 
1284 Message sendAudio(T1)(BotApi api, T1 chatId, string audio)
1285     if (isTelegramId!T1)
1286 {
1287     SendAudioMethod m = {
1288         chat_id : chatId,
1289         audio : audio
1290     };
1291 
1292     return sendAudio(api, m);
1293 }
1294 
1295 Message sendDocument(BotApi api, ref SendDocumentMethod m)
1296 {
1297     return api.callMethod!(Message, SendDocumentMethod)(m);
1298 }
1299 
1300 Message sendDocument(T1)(BotApi api, T1 chatId, string document)
1301     if (isTelegramId!T1)
1302 {
1303     SendDocumentMethod m = {
1304         chat_id : chatId,
1305         document : document
1306     };
1307 
1308     return sendDocument(api, m);
1309 }
1310 
1311 Message sendVideo(BotApi api, ref SendVideoMethod m)
1312 {
1313     return api.callMethod!(Message, SendVideoMethod)(m);
1314 }
1315 
1316 Message sendVideo(T1)(BotApi api, T1 chatId, string video)
1317     if (isTelegramId!T1)
1318 {
1319     SendVideoMethod m = {
1320         chat_id : chatId,
1321         video : video
1322     };
1323 
1324     return sendVideo(api, m);
1325 }
1326 
1327 Message sendVoice(BotApi api, ref SendVoiceMethod m)
1328 {
1329     return api.callMethod!(Message, SendVoiceMethod)(m);
1330 }
1331 
1332 Message sendVoice(T1)(BotApi api, T1 chatId, string voice)
1333     if (isTelegramId!T1)
1334 {
1335     SendVoiceMethod m = {
1336         chat_id : chatId,
1337         voice : voice
1338     };
1339 
1340     return sendVoice(api, m);
1341 }
1342 
1343 Message sendVideoNote(BotApi api, ref SendVideoNoteMethod m)
1344 {
1345     return api.callMethod!(Message, SendVideoNoteMethod)(m);
1346 }
1347 
1348 Message sendVideoNote(T1)(BotApi api, T1 chatId, string videoNote)
1349     if (isTelegramId!T1)
1350 {
1351     SendVideoNoteMethod m = {
1352         chat_id : chatId,
1353         video_note : videoNote
1354     };
1355 
1356     return sendVideoNote(api, m);
1357 }
1358 
1359 Message sendMediaGroup(BotApi api, ref SendMediaGroupMethod m)
1360 {
1361     return api.callMethod!(Message, SendMediaGroupMethod)(m);
1362 }
1363 
1364 Message sendMediaGroup(T1)(BotApi api, T1 chatId, InputMedia[] media)
1365     if (isTelegramId!T1)
1366 {
1367     SendMediaGroupMethod m = {
1368         chat_id : chatId,
1369         media : media
1370     };
1371 
1372     return sendMediaGroup(api, m);
1373 }
1374 
1375 Message sendLocation(BotApi api, ref SendLocationMethod m)
1376 {
1377     return api.callMethod!(Message, SendLocationMethod)(m);
1378 }
1379 
1380 Message sendLocation(T1)(BotApi api, T1 chatId, float latitude, float longitude)
1381     if (isTelegramId!T1)
1382 {
1383     SendLocationMethod m = {
1384         chat_id : chatId,
1385         latitude : latitude,
1386         longitude : longitude,
1387     };
1388 
1389     return sendLocation(api, m);
1390 }
1391 
1392 Nullable!Message editMessageLiveLocation(BotApi api, ref EditMessageLiveLocationMethod m)
1393 {
1394     return api.callMethod!(Nullable!Message, EditMessageLiveLocationMethod)(m);
1395 }
1396 
1397 Nullable!Message editMessageLiveLocation(BotApi api, string inlineMessageId, float latitude, float longitude)
1398 {
1399     EditMessageLiveLocationMethod m = {
1400         inline_message_id : inlineMessageId,
1401         latitude : latitude,
1402         longitude : longitude
1403     };
1404 
1405     return editMessageLiveLocation(api, m);
1406 }
1407 
1408 Nullable!Message editMessageLiveLocation(T1)(BotApi api, T1 chatId, uint messageId, float latitude, float longitude)
1409     if (isTelegramId!T1)
1410 {
1411     EditMessageLiveLocationMethod m = {
1412         chat_id : chatId,
1413         message_id : messageId,
1414         latitude : latitude,
1415         longitude : longitude
1416     };
1417 
1418     return editMessageLiveLocation(api, m);
1419 }
1420 
1421 Nullable!Message stopMessageLiveLocation(BotApi api, ref StopMessageLiveLocationMethod m)
1422 {
1423     return api.callMethod!(Nullable!Message, StopMessageLiveLocationMethod)(m);
1424 }
1425 
1426 Nullable!Message stopMessageLiveLocation(BotApi api, string inlineMessageId)
1427 {
1428     StopMessageLiveLocationMethod m = {
1429         inline_message_id : inlineMessageId
1430     };
1431 
1432     return stopMessageLiveLocation(api, m);
1433 }
1434 
1435 Nullable!Message stopMessageLiveLocation(T1)(BotApi api, T1 chatId, uint messageId)
1436     if (isTelegramId!T1)
1437 {
1438     StopMessageLiveLocationMethod m = {
1439         chat_id : chatId,
1440         message_id : messageId
1441     };
1442 
1443     return stopMessageLiveLocation(api, m);
1444 }
1445 
1446 Message sendVenue(BotApi api, ref SendVenueMethod m)
1447 {
1448     return api.callMethod!(Message, SendVenueMethod)(m);
1449 }
1450 
1451 Message sendVenue(T1)(BotApi api, T1 chatId, float latitude, float longitude,
1452     string title, string address)
1453     if (isTelegramId!T1)
1454 {
1455     SendVenueMethod m = {
1456         chat_id : chatId,
1457         latitude : latitude,
1458         longitude : longitude,
1459         title : title,
1460         address : address
1461     };
1462 
1463     return sendVenue(api, m);
1464 }
1465 
1466 Message sendContact(BotApi api, ref SendContactMethod m)
1467 {
1468     return api.callMethod!(Message, SendContactMethod)(m);
1469 }
1470 
1471 Message sendContact(T1)(BotApi api, T1 chatId, string phone_number, string first_name)
1472     if (isTelegramId!T1)
1473 {
1474     SendContactMethod m = {
1475         chat_id : chatId,
1476         phone_number : phone_number,
1477         first_name : first_name
1478     };
1479 
1480     return sendContact(api, m);
1481 }
1482 
1483 bool sendChatAction(BotApi api, ref SendChatActionMethod m)
1484 {
1485     return api.callMethod!(bool, SendChatActionMethod)(m);
1486 }
1487 
1488 bool sendChatAction(T1)(BotApi api, T1 chatId, string action)
1489     if (isTelegramId!T1)
1490 {
1491     SendChatActionMethod m = {
1492         chat_id : chatId,
1493         action : action
1494     };
1495 
1496     return sendChatAction(api, m);
1497 }
1498 
1499 UserProfilePhotos getUserProfilePhotos(BotApi api, ref GetUserProfilePhotosMethod m)
1500 {
1501     return api.callMethod!(UserProfilePhotos, GetUserProfilePhotosMethod)(m);
1502 }
1503 
1504 UserProfilePhotos getUserProfilePhotos(BotApi api, int userId)
1505 {
1506     GetUserProfilePhotosMethod m = {
1507         user_id : userId
1508     };
1509 
1510     return getUserProfilePhotos(api, m);
1511 }
1512 
1513 File getFile(BotApi api, ref GetFileMethod m)
1514 {
1515     return api.callMethod!(File, GetFileMethod)(m);
1516 }
1517 
1518 File getFile(BotApi api, string fileId)
1519 {
1520     GetFileMethod m = {
1521         file_id : fileId
1522     };
1523 
1524     return getFile(api, m);
1525 }
1526 
1527 
1528 
1529 Chat getChat(BotApi api, ref GetChatMethod m)
1530 {
1531     return api.callMethod!Chat(m);
1532 }
1533 
1534 Chat getChat(T1)(BotApi api, T1 chatId)
1535     if (isTelegramId!T1)
1536 {
1537     GetChatMethod m = {
1538         chat_id : chatId,
1539     };
1540 
1541     return getChat(api, m);
1542 }
1543 
1544 bool answerCallbackQuery(BotApi api, ref AnswerCallbackQueryMethod m)
1545 {
1546     return api.callMethod!bool(m);
1547 }
1548 
1549 bool answerCallbackQuery(BotApi api, string callbackQueryId)
1550 {
1551     AnswerCallbackQueryMethod m = {
1552         callback_query_id : callbackQueryId
1553     };
1554 
1555     return answerCallbackQuery(api, m);
1556 }
1557 
1558 unittest
1559 {
1560     class BotApiMock : BotApi
1561     {
1562         this(string token)
1563         {
1564             super(token);
1565         }
1566 
1567         T callMethod(T, M)(M method)
1568         {
1569             T result;
1570 
1571             logDiagnostic("[%d] Requesting %s", requestCounter, method.name);
1572 
1573             return result;
1574         }
1575     }
1576 
1577     auto api = new BotApiMock(null);
1578 
1579     api.getUpdates(5,30);
1580     api.getMe();
1581     api.sendMessage("chat-id", "hello");
1582     api.forwardMessage("chat-id", "from-chat-id", 123);
1583     api.sendPhoto("chat-id", "photo-url");
1584     api.sendAudio("chat-id", "audio-url");
1585     api.sendDocument("chat-id", "document-url");
1586     api.sendVideo("chat-id", "video-url");
1587     api.sendVoice("chat-id", "voice-url");
1588     api.sendVideoNote("chat-id", "video-note-url");
1589     api.sendMediaGroup("chat-id", []);
1590     api.sendLocation("chat-id", 123, 123);
1591     api.editMessageLiveLocation("chat-id", 1, 1.23, 4.56);
1592     api.editMessageLiveLocation("inline-message-id", 1.23, 4.56);
1593     api.stopMessageLiveLocation("chat-id", 1);
1594     api.stopMessageLiveLocation("inline-message-id");
1595     api.sendVenue("chat-id", 123, 123, "title", "address");
1596     api.sendContact("chat-id", "+123", "First Name");
1597     api.sendChatAction("chat-id", "typing");
1598     api.getUserProfilePhotos(1);
1599     api.getFile("file-id");
1600     api.getChat("chat-id");
1601     api.answerCallbackQuery("callback-query-id");
1602 }