├── .gitattributes ├── .gitignore ├── CalendarPicker.sln ├── CalendarPicker ├── CalendarBot.cs ├── CalendarBotConfiguration.cs ├── CalendarControl │ ├── CalendarControlExtension.cs │ ├── CallbackCommandEx.cs │ ├── Common │ │ ├── Constants.cs │ │ ├── Markup.cs │ │ └── Row.cs │ └── Handlers │ │ ├── Calendar │ │ ├── ChangeToHandler.cs │ │ └── PickDateHandler.cs │ │ ├── CalendarCommand.cs │ │ ├── FaultedUpdateHandler.cs │ │ └── MonthYear │ │ ├── MonthPickerHandler.cs │ │ ├── YearMonthPickerHandlercs.cs │ │ └── YearPickerHandler.cs ├── CalendarPicker.csproj ├── Extensions │ └── AppStartupExtensions.cs ├── Options │ └── CustomBotOptions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── BotServiceProvider.cs │ └── LocalizationService.cs ├── Startup.cs └── appsettings.json ├── LICENSE ├── README.md └── screenshot └── markup_preview.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # ASP Net Core 5 | appsettings.*.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | -------------------------------------------------------------------------------- /CalendarPicker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalendarPicker", "CalendarPicker\CalendarPicker.csproj", "{4C84FD57-8724-47EC-B380-ED9286B591E9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4C84FD57-8724-47EC-B380-ED9286B591E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4C84FD57-8724-47EC-B380-ED9286B591E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4C84FD57-8724-47EC-B380-ED9286B591E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4C84FD57-8724-47EC-B380-ED9286B591E9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9682DF8C-D06C-44A9-8E1B-9E969D07D6EB} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarBot.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using Telegram.Bot.Framework; 4 | 5 | namespace CalendarPicker 6 | { 7 | public class CalendarBot : BotBase 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public CalendarBot(IOptions> botOptions, ILogger logger) 12 | : base(botOptions.Value) 13 | { 14 | _logger = logger; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarBotConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace CalendarPicker 2 | { 3 | public class CalendarBotConfiguration 4 | { 5 | public string BotLocale { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/CalendarControlExtension.cs: -------------------------------------------------------------------------------- 1 | using CalendarPicker.CalendarControl.Handlers; 2 | using CalendarPicker.Options; 3 | using CalendarPicker.Services; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Telegram.Bot.Framework; 7 | 8 | namespace CalendarPicker.CalendarControl 9 | { 10 | public static class CalendarControlExtension 11 | { 12 | public static IServiceCollection AddCalendarBot(this IServiceCollection services, IConfigurationSection botConfiguration) => 13 | services.AddTransient() 14 | .Configure>(botConfiguration) 15 | .Configure>(botConfiguration) 16 | .AddScoped() 17 | .AddScoped() 18 | .AddScoped() 19 | .AddScoped() 20 | .AddScoped() 21 | .AddScoped() 22 | .AddScoped(); 23 | 24 | public static IServiceCollection AddOperationServices(this IServiceCollection services) => 25 | services 26 | .AddTransient(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/CallbackCommandEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Telegram.Bot.Types; 3 | 4 | namespace CalendarPicker.CalendarControl 5 | { 6 | public static class CallbackCommandEx 7 | { 8 | public static bool IsCallbackCommand(this Update update, string command) => 9 | update.CallbackQuery.Data.StartsWith( 10 | command, 11 | StringComparison.Ordinal); 12 | 13 | public static string TrimCallbackCommand(this Update update, string pattern) => 14 | update.CallbackQuery.Data.Replace(pattern, string.Empty); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Common/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace CalendarPicker.CalendarControl 4 | { 5 | public static class Constants 6 | { 7 | public const string ChangeTo = "cng-to/"; 8 | 9 | public const string PickDate = "pck/"; 10 | 11 | public const string YearMonthPicker = "pck-ym/"; 12 | 13 | //public const string MonthPicker = "cng-m/"; 14 | 15 | //public const string YearPicker = "cng-y/"; 16 | 17 | public const string PickMonth = "pck-m/"; 18 | 19 | public const string PickYear = "pck-y/"; 20 | 21 | public const string DateFormat = @"yyyy\/MM\/dd"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Common/Markup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Telegram.Bot.Types.ReplyMarkups; 5 | 6 | namespace CalendarPicker.CalendarControl 7 | { 8 | public static class Markup 9 | { 10 | public static InlineKeyboardMarkup Calendar(in DateTime date, DateTimeFormatInfo dtfi) 11 | { 12 | var keyboardRows = new List>(); 13 | 14 | keyboardRows.Add(Row.Date(date, dtfi)); 15 | keyboardRows.Add(Row.DayOfWeek(dtfi)); 16 | keyboardRows.AddRange(Row.Month(date, dtfi)); 17 | keyboardRows.Add(Row.Controls(date)); 18 | 19 | return new InlineKeyboardMarkup(keyboardRows); 20 | } 21 | 22 | public static InlineKeyboardMarkup PickMonthYear(in DateTime date, DateTimeFormatInfo dtfi) 23 | { 24 | var keyboardRows = new InlineKeyboardButton[][] 25 | { 26 | new InlineKeyboardButton[] 27 | { 28 | InlineKeyboardButton.WithCallbackData( 29 | date.ToString("MMMM", dtfi), 30 | $"{Constants.PickMonth}{date.ToString(Constants.DateFormat)}" 31 | ), 32 | InlineKeyboardButton.WithCallbackData( 33 | date.ToString("yyyy", dtfi), 34 | $"{Constants.PickYear}{date.ToString(Constants.DateFormat)}" 35 | ) 36 | }, 37 | new InlineKeyboardButton[] 38 | { 39 | InlineKeyboardButton.WithCallbackData( 40 | "<<", 41 | $"{Constants.ChangeTo}{date.ToString(Constants.DateFormat)}" 42 | ), 43 | " " 44 | } 45 | }; 46 | 47 | return new InlineKeyboardMarkup(keyboardRows); 48 | } 49 | 50 | public static InlineKeyboardMarkup PickMonth(in DateTime date, DateTimeFormatInfo dtfi) 51 | { 52 | var keyboardRows = new InlineKeyboardButton[5][]; 53 | 54 | for (int month = 0, row = 0; month < 12; row++) 55 | { 56 | var keyboardRow = new InlineKeyboardButton[3]; 57 | for (var j = 0; j < 3; j++, month++) 58 | { 59 | var day = new DateTime(date.Year, month + 1, 1); 60 | 61 | keyboardRow[j] = InlineKeyboardButton.WithCallbackData( 62 | dtfi.MonthNames[month], 63 | $"{Constants.YearMonthPicker}{day.ToString(Constants.DateFormat)}" 64 | ); 65 | } 66 | 67 | keyboardRows[row] = keyboardRow; 68 | } 69 | keyboardRows[4] = Row.BackToMonthYearPicker(date); 70 | 71 | return new InlineKeyboardMarkup(keyboardRows); 72 | } 73 | 74 | public static InlineKeyboardMarkup PickYear(in DateTime date, DateTimeFormatInfo dtfi) 75 | { 76 | var keyboardRows = new InlineKeyboardButton[5][]; 77 | 78 | var startYear = date.AddYears(-7); 79 | 80 | for (int i = 0, row = 0; i < 12; row++) 81 | { 82 | var keyboardRow = new InlineKeyboardButton[3]; 83 | for (var j = 0; j < 3; j++, i++) 84 | { 85 | var day = startYear.AddYears(i); 86 | 87 | keyboardRow[j] = InlineKeyboardButton.WithCallbackData( 88 | day.ToString("yyyy", dtfi), 89 | $"{Constants.YearMonthPicker}{day.ToString(Constants.DateFormat)}" 90 | ); 91 | } 92 | 93 | keyboardRows[row] = keyboardRow; 94 | } 95 | keyboardRows[4] = Row.BackToMonthYearPicker(date); 96 | 97 | return new InlineKeyboardMarkup(keyboardRows); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Common/Row.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Telegram.Bot.Types.ReplyMarkups; 5 | 6 | namespace CalendarPicker.CalendarControl 7 | { 8 | public static class Row 9 | { 10 | public static IEnumerable Date(in DateTime date, DateTimeFormatInfo dtfi) => 11 | new InlineKeyboardButton[] 12 | { 13 | InlineKeyboardButton.WithCallbackData( 14 | $"» {date.ToString("Y", dtfi)} «", 15 | $"{Constants.YearMonthPicker}{date.ToString(Constants.DateFormat)}" 16 | ) 17 | }; 18 | 19 | public static IEnumerable DayOfWeek(DateTimeFormatInfo dtfi) 20 | { 21 | var dayNames = new InlineKeyboardButton[7]; 22 | 23 | var firstDayOfWeek = (int)dtfi.FirstDayOfWeek; 24 | for (int i = 0; i < 7; i++) 25 | { 26 | yield return dtfi.AbbreviatedDayNames[(firstDayOfWeek + i) % 7]; 27 | } 28 | } 29 | 30 | public static IEnumerable> Month(DateTime date, DateTimeFormatInfo dtfi) 31 | { 32 | var firstDayOfMonth = new DateTime(date.Year, date.Month, 1); 33 | var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1).Day; 34 | 35 | for (int dayOfMonth = 1, weekNum = 0; dayOfMonth <= lastDayOfMonth; weekNum++) 36 | { 37 | yield return NewWeek(weekNum, ref dayOfMonth); 38 | } 39 | 40 | IEnumerable NewWeek(int weekNum, ref int dayOfMonth) 41 | { 42 | var week = new InlineKeyboardButton[7]; 43 | 44 | for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) 45 | { 46 | if ((weekNum == 0 && dayOfWeek < FirstDayOfWeek()) 47 | || 48 | dayOfMonth > lastDayOfMonth 49 | ) 50 | { 51 | week[dayOfWeek] = " "; 52 | continue; 53 | } 54 | 55 | week[dayOfWeek] = InlineKeyboardButton.WithCallbackData( 56 | dayOfMonth.ToString(), 57 | $"{Constants.PickDate}{date.ToString(Constants.DateFormat)}" 58 | ); 59 | 60 | dayOfMonth++; 61 | } 62 | return week; 63 | 64 | int FirstDayOfWeek() => 65 | (7 + (int)firstDayOfMonth.DayOfWeek - (int)dtfi.FirstDayOfWeek) % 7; 66 | } 67 | } 68 | 69 | public static IEnumerable Controls(in DateTime date) => 70 | new InlineKeyboardButton[] 71 | { 72 | InlineKeyboardButton.WithCallbackData( 73 | "<", 74 | $"{Constants.ChangeTo}{date.AddMonths(-1).ToString(Constants.DateFormat)}" 75 | ), 76 | " ", 77 | InlineKeyboardButton.WithCallbackData( 78 | ">", 79 | $"{Constants.ChangeTo}{date.AddMonths(1).ToString(Constants.DateFormat)}" 80 | ), 81 | }; 82 | 83 | public static InlineKeyboardButton[] BackToMonthYearPicker(in DateTime date) => 84 | new InlineKeyboardButton[3] 85 | { 86 | InlineKeyboardButton.WithCallbackData( 87 | "<<", 88 | $"{Constants.YearMonthPicker}{date.ToString(Constants.DateFormat)}" 89 | ), 90 | " ", 91 | " " 92 | }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/Calendar/ChangeToHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CalendarPicker.Services; 6 | using Telegram.Bot.Framework.Abstractions; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace CalendarPicker.CalendarControl.Handlers 10 | { 11 | public class ChangeToHandler : IUpdateHandler 12 | { 13 | private readonly LocalizationService _locale; 14 | 15 | public ChangeToHandler(LocalizationService locale) 16 | { 17 | _locale = locale; 18 | } 19 | 20 | public static bool CanHandle(IUpdateContext context) 21 | { 22 | return 23 | context.Update.Type == UpdateType.CallbackQuery 24 | && 25 | context.Update.IsCallbackCommand(Constants.ChangeTo); 26 | } 27 | 28 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 29 | { 30 | if (!DateTime.TryParseExact( 31 | context.Update.TrimCallbackCommand(Constants.ChangeTo), 32 | Constants.DateFormat, 33 | null, 34 | DateTimeStyles.None, 35 | out var date) 36 | ) 37 | { 38 | return; 39 | } 40 | 41 | var calendarMarkup = Markup.Calendar(date, _locale.DateCulture); 42 | 43 | await context.Bot.Client.EditMessageReplyMarkupAsync( 44 | context.Update.CallbackQuery.Message.Chat.Id, 45 | context.Update.CallbackQuery.Message.MessageId, 46 | replyMarkup: calendarMarkup 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/Calendar/PickDateHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CalendarPicker.Services; 6 | using Telegram.Bot.Framework.Abstractions; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace CalendarPicker.CalendarControl.Handlers 10 | { 11 | public class PickDateHandler : IUpdateHandler 12 | { 13 | private readonly LocalizationService _locale; 14 | 15 | public PickDateHandler(LocalizationService locale) 16 | { 17 | _locale = locale; 18 | } 19 | 20 | public static bool CanHandle(IUpdateContext context) 21 | { 22 | return 23 | context.Update.Type == UpdateType.CallbackQuery 24 | && 25 | context.Update.IsCallbackCommand(Constants.PickDate); 26 | } 27 | 28 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 29 | { 30 | if (!DateTime.TryParseExact( 31 | context.Update.TrimCallbackCommand(Constants.PickDate), 32 | Constants.DateFormat, 33 | null, 34 | DateTimeStyles.None, 35 | out var date) 36 | ) 37 | { 38 | return; 39 | } 40 | 41 | await context.Bot.Client.SendTextMessageAsync( 42 | context.Update.CallbackQuery.Message.Chat.Id, 43 | date.ToString("d", _locale.DateCulture) 44 | ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/CalendarCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CalendarPicker.Services; 5 | using Telegram.Bot.Framework.Abstractions; 6 | 7 | namespace CalendarPicker.CalendarControl.Handlers 8 | { 9 | public class CalendarCommand : CommandBase 10 | { 11 | private readonly LocalizationService _locale; 12 | 13 | public CalendarCommand(LocalizationService locale) 14 | { 15 | _locale = locale; 16 | } 17 | 18 | public override async Task HandleAsync( 19 | IUpdateContext context, 20 | UpdateDelegate next, 21 | string[] args, 22 | CancellationToken cancellationToken 23 | ) 24 | { 25 | var calendarMarkup = Markup.Calendar(DateTime.Today, _locale.DateCulture); 26 | 27 | await context.Bot.Client.SendTextMessageAsync( 28 | context.Update.Message.Chat.Id, 29 | "Pick date:", 30 | replyMarkup: calendarMarkup 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/FaultedUpdateHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using Telegram.Bot.Framework.Abstractions; 6 | 7 | namespace CalendarPicker.CalendarControl.Handlers 8 | { 9 | public class FaultedUpdateHandler : IUpdateHandler 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public FaultedUpdateHandler(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 19 | { 20 | try 21 | { 22 | await next(context, cancellationToken).ConfigureAwait(false); 23 | } 24 | catch (Exception e) 25 | { 26 | _logger.LogError("Exception occured in handling update of type `{0}`: {1}", context.Update.Type, e.Message); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/MonthYear/MonthPickerHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CalendarPicker.Services; 6 | using Telegram.Bot.Framework.Abstractions; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace CalendarPicker.CalendarControl.Handlers 10 | { 11 | public class MonthPickerHandler : IUpdateHandler 12 | { 13 | private readonly LocalizationService _locale; 14 | 15 | public MonthPickerHandler(LocalizationService locale) 16 | { 17 | _locale = locale; 18 | } 19 | 20 | public static bool CanHandle(IUpdateContext context) 21 | { 22 | return 23 | context.Update.Type == UpdateType.CallbackQuery 24 | && 25 | context.Update.IsCallbackCommand(Constants.PickMonth); 26 | } 27 | 28 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 29 | { 30 | if (!DateTime.TryParseExact( 31 | context.Update.TrimCallbackCommand(Constants.PickMonth), 32 | Constants.DateFormat, 33 | null, 34 | DateTimeStyles.None, 35 | out var date) 36 | ) 37 | { 38 | return; 39 | } 40 | 41 | var monthPickerMarkup = Markup.PickMonth(date, _locale.DateCulture); 42 | 43 | await context.Bot.Client.EditMessageReplyMarkupAsync( 44 | context.Update.CallbackQuery.Message.Chat.Id, 45 | context.Update.CallbackQuery.Message.MessageId, 46 | replyMarkup: monthPickerMarkup 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/MonthYear/YearMonthPickerHandlercs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CalendarPicker.Services; 6 | using Telegram.Bot.Framework.Abstractions; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace CalendarPicker.CalendarControl.Handlers 10 | { 11 | public class YearMonthPickerHandler : IUpdateHandler 12 | { 13 | private readonly LocalizationService _locale; 14 | 15 | public YearMonthPickerHandler(LocalizationService locale) 16 | { 17 | _locale = locale; 18 | } 19 | 20 | public static bool CanHandle(IUpdateContext context) 21 | { 22 | return 23 | context.Update.Type == UpdateType.CallbackQuery 24 | && 25 | context.Update.IsCallbackCommand(Constants.YearMonthPicker); 26 | } 27 | 28 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 29 | { 30 | if (!DateTime.TryParseExact( 31 | context.Update.TrimCallbackCommand(Constants.YearMonthPicker), 32 | Constants.DateFormat, 33 | null, 34 | DateTimeStyles.None, 35 | out var date) 36 | ) 37 | { 38 | return; 39 | } 40 | 41 | var monthYearMarkup = Markup.PickMonthYear(date, _locale.DateCulture); 42 | 43 | await context.Bot.Client.EditMessageReplyMarkupAsync( 44 | context.Update.CallbackQuery.Message.Chat.Id, 45 | context.Update.CallbackQuery.Message.MessageId, 46 | replyMarkup: monthYearMarkup 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarControl/Handlers/MonthYear/YearPickerHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CalendarPicker.Services; 6 | using Telegram.Bot.Framework.Abstractions; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | namespace CalendarPicker.CalendarControl.Handlers 10 | { 11 | public class YearPickerHandler : IUpdateHandler 12 | { 13 | private readonly LocalizationService _locale; 14 | 15 | public YearPickerHandler(LocalizationService locale) 16 | { 17 | _locale = locale; 18 | } 19 | 20 | public static bool CanHandle(IUpdateContext context) 21 | { 22 | return 23 | context.Update.Type == UpdateType.CallbackQuery 24 | && 25 | context.Update.IsCallbackCommand(Constants.PickYear); 26 | } 27 | 28 | public async Task HandleAsync(IUpdateContext context, UpdateDelegate next, CancellationToken cancellationToken) 29 | { 30 | if (!DateTime.TryParseExact( 31 | context.Update.TrimCallbackCommand(Constants.PickYear), 32 | Constants.DateFormat, 33 | null, 34 | DateTimeStyles.None, 35 | out var date) 36 | ) 37 | { 38 | return; 39 | } 40 | 41 | var monthPickerMarkup = Markup.PickYear(date, _locale.DateCulture); 42 | 43 | await context.Bot.Client.EditMessageReplyMarkupAsync( 44 | context.Update.CallbackQuery.Message.Chat.Id, 45 | context.Update.CallbackQuery.Message.MessageId, 46 | replyMarkup: monthPickerMarkup 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CalendarPicker/CalendarPicker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CalendarPicker/Extensions/AppStartupExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CalendarPicker; 5 | using CalendarPicker.Options; 6 | using CalendarPicker.Services; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | using Telegram.Bot.Framework; 11 | using Telegram.Bot.Framework.Abstractions; 12 | 13 | namespace Microsoft.AspNetCore.Builder 14 | { 15 | internal static class AppStartupExtensions 16 | { 17 | public static void UseTelegramBotLongPolling( 18 | this IApplicationBuilder app, 19 | IBotBuilder botBuilder, 20 | TimeSpan startAfter = default, 21 | CancellationToken cancellationToken = default 22 | ) 23 | where TBot : BotBase 24 | { 25 | var logger = app.ApplicationServices.GetRequiredService>(); 26 | if (startAfter == default) 27 | { 28 | startAfter = TimeSpan.FromSeconds(2); 29 | } 30 | 31 | var updateManager = new UpdatePollingManager(botBuilder, new BotServiceProvider(app)); 32 | 33 | Task.Run(async () => 34 | { 35 | await Task.Delay(startAfter, cancellationToken) 36 | .ConfigureAwait(false); 37 | await updateManager.RunAsync(cancellationToken: cancellationToken) 38 | .ConfigureAwait(false); 39 | }, 40 | cancellationToken) 41 | .ContinueWith(t => 42 | { 43 | logger.LogError(t.Exception, "Bot update manager failed."); 44 | throw t.Exception; 45 | }, TaskContinuationOptions.OnlyOnFaulted 46 | ); 47 | } 48 | 49 | public static void EnsureWebhookSet(this IApplicationBuilder app) 50 | where TBot : IBot 51 | { 52 | using (var scope = app.ApplicationServices.CreateScope()) 53 | { 54 | var logger = scope.ServiceProvider.GetRequiredService>(); 55 | var bot = scope.ServiceProvider.GetRequiredService(); 56 | var options = scope.ServiceProvider.GetRequiredService>>(); 57 | var url = new Uri(new Uri(options.Value.WebhookDomain), options.Value.WebhookPath); 58 | 59 | logger.LogDebug("Setting webhook for bot \"{0}\" to URL \"{1}\"", typeof(TBot).Name, url); 60 | 61 | bot.Client.SetWebhookAsync(url.AbsoluteUri) 62 | .GetAwaiter().GetResult(); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /CalendarPicker/Options/CustomBotOptions.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Framework; 2 | using Telegram.Bot.Framework.Abstractions; 3 | 4 | namespace CalendarPicker.Options 5 | { 6 | public class CustomBotOptions : BotOptions 7 | where TBot : IBot 8 | { 9 | public string WebhookDomain { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CalendarPicker/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace CalendarPicker 5 | { 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var host = new WebHostBuilder() 11 | .UseKestrel() 12 | .UseContentRoot(Directory.GetCurrentDirectory()) 13 | .UseIISIntegration() 14 | .UseStartup() 15 | .UseApplicationInsights() 16 | .Build(); 17 | 18 | host.Run(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CalendarPicker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:6425/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchUrl": "api/values", 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "CalendarPicker": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "api/values", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "http://localhost:6426/" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /CalendarPicker/Services/BotServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Telegram.Bot.Framework.Abstractions; 5 | 6 | namespace CalendarPicker.Services 7 | { 8 | internal class BotServiceProvider : IBotServiceProvider 9 | { 10 | private readonly IServiceProvider _container; 11 | 12 | private readonly IServiceScope _scope; 13 | 14 | public BotServiceProvider(IApplicationBuilder app) 15 | { 16 | _container = app.ApplicationServices; 17 | } 18 | 19 | public BotServiceProvider(IServiceScope scope) 20 | { 21 | _scope = scope; 22 | } 23 | 24 | public object GetService(Type serviceType) => 25 | _scope != null 26 | ? _scope.ServiceProvider.GetService(serviceType) 27 | : _container.GetService(serviceType) 28 | ; 29 | 30 | public IBotServiceProvider CreateScope() => 31 | new BotServiceProvider(_container.CreateScope()); 32 | 33 | public void Dispose() 34 | { 35 | _scope?.Dispose(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CalendarPicker/Services/LocalizationService.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace CalendarPicker.Services 4 | { 5 | public class LocalizationService 6 | { 7 | private readonly CalendarBotConfiguration _configuration; 8 | 9 | public DateTimeFormatInfo DateCulture; 10 | 11 | public LocalizationService(CalendarBotConfiguration configuration) 12 | { 13 | _configuration = configuration; 14 | 15 | DateCulture = configuration.BotLocale == null 16 | ? new CultureInfo("en-US", false).DateTimeFormat 17 | : new CultureInfo(configuration.BotLocale, false).DateTimeFormat; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CalendarPicker/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CalendarPicker.CalendarControl; 4 | using CalendarPicker.CalendarControl.Handlers; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using Telegram.Bot.Framework; 12 | using Telegram.Bot.Framework.Abstractions; 13 | 14 | namespace CalendarPicker 15 | { 16 | public class Startup 17 | { 18 | public Startup(IHostingEnvironment env) 19 | { 20 | var builder = new ConfigurationBuilder() 21 | .SetBasePath(env.ContentRootPath) 22 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 23 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 24 | .AddEnvironmentVariables(); 25 | _configuration = builder.Build(); 26 | } 27 | 28 | private IConfiguration _configuration { get; } 29 | 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddCalendarBot(_configuration.GetSection("CalendarBot")); 33 | 34 | // Add configuration 35 | services.AddSingleton( 36 | _configuration.GetSection("CalendarBot").Get() 37 | ); 38 | 39 | services.AddOperationServices(); 40 | } 41 | 42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | 48 | app.UseTelegramBotLongPolling(ConfigureBot(), startAfter: TimeSpan.FromSeconds(2)); 49 | } 50 | else 51 | { 52 | app.UseTelegramBotWebhook(ConfigureBot()); 53 | app.EnsureWebhookSet(); 54 | } 55 | 56 | app.Run(async context => await context.Response.WriteAsync("Hello World!")); 57 | } 58 | 59 | private IBotBuilder ConfigureBot() => 60 | new BotBuilder() 61 | .Use() 62 | .UseCommand("calendar") 63 | .UseWhen(ChangeToHandler.CanHandle) 64 | .UseWhen(PickDateHandler.CanHandle) 65 | .UseWhen(YearMonthPickerHandler.CanHandle) 66 | .UseWhen(MonthPickerHandler.CanHandle) 67 | .UseWhen(YearPickerHandler.CanHandle) 68 | ; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CalendarPicker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CalendarBot": { 3 | "Username": "{your-bots-username}", 4 | "ApiToken": "{your-bots-api-token}", 5 | "WebhookDomain": "https://example.com/bots/{bot}/webhook/{token}", 6 | "WebhookPath": "/api/bots/1234567:4TT8bAc8GHUspu3ERYn-KGcvsvGB9u_n4ddy/webhook", 7 | 8 | "BotLocale": "es-ES" 9 | }, 10 | "Logging": { 11 | "IncludeScopes": false, 12 | "Debug": { 13 | "LogLevel": { 14 | "Default": "Warning" 15 | } 16 | }, 17 | "Console": { 18 | "LogLevel": { 19 | "Default": "Warning" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 karb0f0s 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot with Calendar Picker 2 | ## About 3 | Simple Telegram Bot with Calendar Picker control. Based on [Telegram.Bot.Framework](https://github.com/TelegramBots/Telegram.Bot.Framework) 4 | 5 | ## Configuration 6 | Just modify appsettings.json and run. 7 | The only mandatory parameter is `ApiToken`. 8 | Default `BotLocale` is `"en-US"`, but you can play with [other options](https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx): 9 | ```javascript 10 | { 11 | "CalendarBot": { 12 | "ApiToken": "{your-bots-api-token}", 13 | "BotUserName": "{your-bots-username}", 14 | "PathToCertificate": "", 15 | "WebhookUrl": "https://example.com/bots/{bot}/webhook/{token}", 16 | 17 | "BotLocale": "es-ES" 18 | }, 19 | } 20 | ``` 21 | 22 | ## Screenshots 23 | ![](screenshot/markup_preview.png) 24 | -------------------------------------------------------------------------------- /screenshot/markup_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karb0f0s/CalendarPicker/5586f29c5535ac873608d8bd41fb05ae3bdc4c1e/screenshot/markup_preview.png --------------------------------------------------------------------------------