From 6a5461041947236d5f6db249a6a3f013f85eecf3 Mon Sep 17 00:00:00 2001 From: Ahmad Tohir Date: Sat, 22 Oct 2022 03:06:53 +0700 Subject: [PATCH 1/3] feat(place-search): add search place and show the static map --- BotNet.Services/BotCommands/SearchPlace.cs | 60 +++++++++++++ BotNet.Services/GoogleMap/GeoCode.cs | 85 +++++++++++++++++++ BotNet.Services/GoogleMap/GoogleMapOptions.cs | 5 ++ BotNet.Services/GoogleMap/Models/Geometry.cs | 13 +++ .../GoogleMap/Models/LocationType.cs | 8 ++ BotNet.Services/GoogleMap/Models/Response.cs | 12 +++ BotNet.Services/GoogleMap/Models/Result.cs | 8 ++ .../GoogleMap/Models/StatusCode.cs | 11 +++ BotNet.Services/GoogleMap/Models/ZoomLevel.cs | 9 ++ .../GoogleMap/ServiceCollectionExtensions.cs | 12 +++ BotNet.Services/GoogleMap/StaticMap.cs | 41 +++++++++ BotNet/Bot/UpdateHandler.cs | 3 + BotNet/Program.cs | 9 +- README.md | 1 + 14 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 BotNet.Services/BotCommands/SearchPlace.cs create mode 100644 BotNet.Services/GoogleMap/GeoCode.cs create mode 100644 BotNet.Services/GoogleMap/GoogleMapOptions.cs create mode 100644 BotNet.Services/GoogleMap/Models/Geometry.cs create mode 100644 BotNet.Services/GoogleMap/Models/LocationType.cs create mode 100644 BotNet.Services/GoogleMap/Models/Response.cs create mode 100644 BotNet.Services/GoogleMap/Models/Result.cs create mode 100644 BotNet.Services/GoogleMap/Models/StatusCode.cs create mode 100644 BotNet.Services/GoogleMap/Models/ZoomLevel.cs create mode 100644 BotNet.Services/GoogleMap/ServiceCollectionExtensions.cs create mode 100644 BotNet.Services/GoogleMap/StaticMap.cs diff --git a/BotNet.Services/BotCommands/SearchPlace.cs b/BotNet.Services/BotCommands/SearchPlace.cs new file mode 100644 index 0000000..6d42dc9 --- /dev/null +++ b/BotNet.Services/BotCommands/SearchPlace.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BotNet.Services.GoogleMap; +using BotNet.Services.RateLimit; +using Microsoft.Extensions.DependencyInjection; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace BotNet.Services.BotCommands { + public static class SearchPlace { + private static readonly RateLimiter SEARCH_PLACE_LIMITER = RateLimiter.PerChat(2, TimeSpan.FromMinutes(2)); + + public static async Task SearchPlaceAsync(ITelegramBotClient telegramBotClient, IServiceProvider serviceProvider, Message message, CancellationToken cancellationToken) { + if (message.Entities?.FirstOrDefault() is { Type: MessageEntityType.BotCommand, Offset: 0, Length: int commandLength } + && message.Text![commandLength..].Trim() is string commandArgument) { + + if (commandArgument.Length > 0) { + try { + SEARCH_PLACE_LIMITER.ValidateActionRate(message.Chat.Id, message.From!.Id); + + try { + string coords = await serviceProvider.GetRequiredService().SearchPlaceAsync(commandArgument); + string staticMapUrl = serviceProvider.GetRequiredService().SearchPlace(commandArgument); + + await telegramBotClient.SendPhotoAsync( + chatId: message.Chat.Id, + photo: staticMapUrl, + caption: coords, + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); + } catch { + await telegramBotClient.SendTextMessageAsync( + chatId: message.Chat.Id, + text: "Lokasi tidak dapat ditemukan", + parseMode: ParseMode.Html, + replyToMessageId: message.MessageId); + } + } catch (RateLimitExceededException exc) when (exc is { Cooldown: var cooldown }) { + await telegramBotClient.SendTextMessageAsync( + chatId: message.Chat.Id, + text: $"Anda belum mendapat giliran. Coba lagi {cooldown}.", + parseMode: ParseMode.Html, + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); + } + } + } else { + await telegramBotClient.SendTextMessageAsync( + chatId: message.Chat.Id, + text: "Silakan masukkan lokasi di depan perinta /search_place", + parseMode: ParseMode.Html, + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); + } + } + } +} diff --git a/BotNet.Services/GoogleMap/GeoCode.cs b/BotNet.Services/GoogleMap/GeoCode.cs new file mode 100644 index 0000000..52c07af --- /dev/null +++ b/BotNet.Services/GoogleMap/GeoCode.cs @@ -0,0 +1,85 @@ +using System; +using System.Net; +using System.Text.Json; +using System.Net.Http; +using Microsoft.Extensions.Options; +using BotNet.Services.GoogleMap.Models; +using System.IO; +using System.Threading.Tasks; + +namespace BotNet.Services.GoogleMap { + + /// + /// This class intended to get geocoding from address. + /// + public class GeoCode { + + + private readonly string? _apiKey; + private string _uriTemplate = "https://maps.googleapis.com/maps/api/geocode/json"; + private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClient _httpClient; + + + public GeoCode(IOptions options) { + _apiKey = options.Value.ApiKey; + _httpClientHandler = new(); + _httpClient = new(_httpClientHandler); + } + + /// + /// The response of this api call is consist of 2 parts. + /// Array of "results" and string of "status" + /// + /// Even though the results is array, the docs say normally the result will have only one element. + /// So, we can grab the result like result[0] + /// + /// Place or address that you want to search + /// strings of coordinates + /// + public async Task SearchPlaceAsync(string? place) { + if (string.IsNullOrEmpty(place)) { + return "Invalid place"; + } + + if (string.IsNullOrEmpty(_apiKey)) { + return "Api key is needed"; + } + + Uri uri = new(_uriTemplate + $"?address={place}&key={_apiKey}"); + HttpResponseMessage response = await _httpClient.GetAsync(uri.AbsoluteUri); + + if (response is not { StatusCode: HttpStatusCode.OK, Content.Headers.ContentType.MediaType: string contentType }) { + throw new HttpRequestException("Unable to find location."); + } + + if (response.Content is not object && contentType is not "application/json") { + throw new HttpRequestException("Failed to parse result."); + } + + Stream bodyContent = await response.Content!.ReadAsStreamAsync(); + + Response? body = await JsonSerializer.DeserializeAsync(bodyContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (body is null) { + throw new HttpRequestException("Failed to parse result."); + } + + if (body.Status is not "OK") { + throw new HttpRequestException("Unable to find location."); + } + + if (body.Results!.Count <= 0) { + throw new HttpRequestException("No Result."); + } + + Result? result = body.Results[0]; + + string lat = result.Geometry!.Location!.Lat.ToString(); + string longitude = result.Geometry!.Location!.Lng.ToString(); + string coordinates = $"lat: {lat}, long: {longitude}"; + + return coordinates; + } + } +} diff --git a/BotNet.Services/GoogleMap/GoogleMapOptions.cs b/BotNet.Services/GoogleMap/GoogleMapOptions.cs new file mode 100644 index 0000000..987f519 --- /dev/null +++ b/BotNet.Services/GoogleMap/GoogleMapOptions.cs @@ -0,0 +1,5 @@ +namespace BotNet.Services.GoogleMap { + public class GoogleMapOptions { + public string? ApiKey { get; set; } + } +} diff --git a/BotNet.Services/GoogleMap/Models/Geometry.cs b/BotNet.Services/GoogleMap/Models/Geometry.cs new file mode 100644 index 0000000..1a7335b --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/Geometry.cs @@ -0,0 +1,13 @@ +namespace BotNet.Services.GoogleMap.Models { + public class Geometry { + + public Coordinate? Location{ get; set; } + + public string? Location_Type { get; set; } + + public class Coordinate { + public double Lat { get; set; } + public double Lng { get; set; } + } + } +} diff --git a/BotNet.Services/GoogleMap/Models/LocationType.cs b/BotNet.Services/GoogleMap/Models/LocationType.cs new file mode 100644 index 0000000..185303a --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/LocationType.cs @@ -0,0 +1,8 @@ +namespace BotNet.Services.GoogleMap.Models { + public enum LocationType { + ROOFTOP, + RANGE_INTERPOLATED, + GEOMETRIC_CENTER, + APPROXIMATE + } +} diff --git a/BotNet.Services/GoogleMap/Models/Response.cs b/BotNet.Services/GoogleMap/Models/Response.cs new file mode 100644 index 0000000..5f0452c --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/Response.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace BotNet.Services.GoogleMap.Models { + + /// + /// Response for google map api geocoding + /// + public class Response { + public List? Results { get; set; } + public string? Status { get; set; } + } +} diff --git a/BotNet.Services/GoogleMap/Models/Result.cs b/BotNet.Services/GoogleMap/Models/Result.cs new file mode 100644 index 0000000..d83aaf9 --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/Result.cs @@ -0,0 +1,8 @@ +namespace BotNet.Services.GoogleMap.Models { + + public class Result { + public string? Formatted_Address { get; set; } + + public Geometry? Geometry { get; set; } + } +} diff --git a/BotNet.Services/GoogleMap/Models/StatusCode.cs b/BotNet.Services/GoogleMap/Models/StatusCode.cs new file mode 100644 index 0000000..305fcda --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/StatusCode.cs @@ -0,0 +1,11 @@ +namespace BotNet.Services.GoogleMap.Models { + public enum StatusCode { + OK, + ZERO_RESULTS, + OVER_DAILY_LIMIT, + OVER_QUERY_LIMIT, + REQUEST_DENIED, + INVALID_REQUEST, + UNKNOWN_ERROR + } +} diff --git a/BotNet.Services/GoogleMap/Models/ZoomLevel.cs b/BotNet.Services/GoogleMap/Models/ZoomLevel.cs new file mode 100644 index 0000000..9d25621 --- /dev/null +++ b/BotNet.Services/GoogleMap/Models/ZoomLevel.cs @@ -0,0 +1,9 @@ +namespace BotNet.Services.GoogleMap.Models { + public enum ZoomLevel { + World = 1, + LandMass = 5, + City = 10, + Streets = 15, + Buildings = 20 + } +} diff --git a/BotNet.Services/GoogleMap/ServiceCollectionExtensions.cs b/BotNet.Services/GoogleMap/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ae917dc --- /dev/null +++ b/BotNet.Services/GoogleMap/ServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace BotNet.Services.GoogleMap { + public static class ServiceCollectionExtensions { + public static IServiceCollection AddGoogleMaps(this IServiceCollection services) { + services.AddTransient(); + services.AddTransient(); + + return services; + } + } +} diff --git a/BotNet.Services/GoogleMap/StaticMap.cs b/BotNet.Services/GoogleMap/StaticMap.cs new file mode 100644 index 0000000..66295fc --- /dev/null +++ b/BotNet.Services/GoogleMap/StaticMap.cs @@ -0,0 +1,41 @@ +using System; +using BotNet.Services.GoogleMap.Models; +using Microsoft.Extensions.Options; + +namespace BotNet.Services.GoogleMap { + + /// + /// Get static map image from google map api + /// + public class StaticMap { + private readonly string? _apiKey; + protected string mapPosition = "center"; + protected int zoom = (int)ZoomLevel.Streets; + protected string size = "600x300"; + protected string marker = "color:red"; + private string _uriTemplate = "https://maps.googleapis.com/maps/api/staticmap"; + + public StaticMap(IOptions options) { + _apiKey = options.Value.ApiKey; + } + + /// + /// Get static map image from google map api + /// + /// Place or address that you want to search + /// string of url + public string SearchPlace(string? place) { + if (string.IsNullOrEmpty(place)) { + return "Invalid place"; + } + + if (string.IsNullOrEmpty(_apiKey)) { + return "Api key is needed"; + } + + Uri uri = new(_uriTemplate + $"?{mapPosition}={place}&zoom={zoom}&size={size}&markers={marker}|{place}&key={_apiKey}"); + + return uri.ToString(); + } + } +} diff --git a/BotNet/Bot/UpdateHandler.cs b/BotNet/Bot/UpdateHandler.cs index 6f60e78..6c6a911 100644 --- a/BotNet/Bot/UpdateHandler.cs +++ b/BotNet/Bot/UpdateHandler.cs @@ -245,6 +245,9 @@ await botClient.SendTextMessageAsync( case "/pse": await Services.BotCommands.PSE.SearchAsync(botClient, _serviceProvider, update.Message, cancellationToken); break; + case "/search_place": + await SearchPlace.SearchPlaceAsync(botClient, _serviceProvider, update.Message, cancellationToken); + break; } } break; diff --git a/BotNet/Program.cs b/BotNet/Program.cs index 8439945..d3a1e60 100644 --- a/BotNet/Program.cs +++ b/BotNet/Program.cs @@ -6,6 +6,7 @@ using BotNet.Services.ColorCard; using BotNet.Services.Craiyon; using BotNet.Services.DynamicExpresso; +using BotNet.Services.GoogleMap; using BotNet.Services.Hosting; using BotNet.Services.ImageConverter; using BotNet.Services.OpenAI; @@ -45,6 +46,7 @@ services.Configure(configuration.GetSection("PistonOptions")); services.Configure(configuration.GetSection("OpenAIOptions")); services.Configure(configuration.GetSection("StabilityOptions")); + services.Configure(configuration.GetSection("GoogleMapOptions")); services.AddHttpClient(); services.AddTenorClient(); services.AddFontService(); @@ -59,16 +61,17 @@ services.AddTiktokServices(); services.AddCSharpEvaluator(); services.AddThisXDoesNotExist(); - services.AddPSEClient(); + //services.AddPSEClient(); services.AddCraiyonClient(); services.AddStabilityClient(); + services.AddGoogleMaps(); // Hosted Services services.Configure(configuration.GetSection("BotOptions")); services.AddSingleton(); - services.AddSingleton(); + //services.AddSingleton(); services.AddHostedService(); - services.AddHostedService(); + //services.AddHostedService(); // Telegram Bot services.AddTelegramBot(botToken: configuration["BotOptions:AccessToken"]); diff --git a/README.md b/README.md index 7cc65b8..0797053 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Telegram Bot written in .NET ```json { "BotOptions:AccessToken": "yourtoken", + "GoogleMapOptions:ApiKey": "yourApiKey", "HostingOptions:UseLongPolling": true } ``` From 7b788208ef177e1ed610ba95891105efb0fab64e Mon Sep 17 00:00:00 2001 From: Ahmad Tohir Date: Sat, 22 Oct 2022 03:29:04 +0700 Subject: [PATCH 2/3] refactor(place-search): bring back pse --- BotNet/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BotNet/Program.cs b/BotNet/Program.cs index d3a1e60..7239504 100644 --- a/BotNet/Program.cs +++ b/BotNet/Program.cs @@ -61,7 +61,7 @@ services.AddTiktokServices(); services.AddCSharpEvaluator(); services.AddThisXDoesNotExist(); - //services.AddPSEClient(); + services.AddPSEClient(); services.AddCraiyonClient(); services.AddStabilityClient(); services.AddGoogleMaps(); @@ -69,9 +69,9 @@ // Hosted Services services.Configure(configuration.GetSection("BotOptions")); services.AddSingleton(); - //services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); - //services.AddHostedService(); + services.AddHostedService(); // Telegram Bot services.AddTelegramBot(botToken: configuration["BotOptions:AccessToken"]); From f45c6b4bfb0b85226b58d71347de746544d0b520 Mon Sep 17 00:00:00 2001 From: Ahmad Tohir Date: Mon, 24 Oct 2022 17:02:35 +0700 Subject: [PATCH 3/3] refactor(place-search): change command from /search_place to /map --- BotNet.Services/BotCommands/SearchPlace.cs | 9 ++++++++- BotNet/Bot/UpdateHandler.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/BotNet.Services/BotCommands/SearchPlace.cs b/BotNet.Services/BotCommands/SearchPlace.cs index 6d42dc9..3c2d0d3 100644 --- a/BotNet.Services/BotCommands/SearchPlace.cs +++ b/BotNet.Services/BotCommands/SearchPlace.cs @@ -46,11 +46,18 @@ await telegramBotClient.SendTextMessageAsync( replyToMessageId: message.MessageId, cancellationToken: cancellationToken); } + } else { + await telegramBotClient.SendTextMessageAsync( + chatId: message.Chat.Id, + text: "Silakan masukkan lokasi di depan perintah /map", + parseMode: ParseMode.Html, + replyToMessageId: message.MessageId, + cancellationToken: cancellationToken); } } else { await telegramBotClient.SendTextMessageAsync( chatId: message.Chat.Id, - text: "Silakan masukkan lokasi di depan perinta /search_place", + text: "Silakan masukkan lokasi di depan perintah /map", parseMode: ParseMode.Html, replyToMessageId: message.MessageId, cancellationToken: cancellationToken); diff --git a/BotNet/Bot/UpdateHandler.cs b/BotNet/Bot/UpdateHandler.cs index 6c6a911..a2347a0 100644 --- a/BotNet/Bot/UpdateHandler.cs +++ b/BotNet/Bot/UpdateHandler.cs @@ -245,7 +245,7 @@ await botClient.SendTextMessageAsync( case "/pse": await Services.BotCommands.PSE.SearchAsync(botClient, _serviceProvider, update.Message, cancellationToken); break; - case "/search_place": + case "/map": await SearchPlace.SearchPlaceAsync(botClient, _serviceProvider, update.Message, cancellationToken); break; }