Skip to content

Commit

Permalink
Merge pull request #51 from oinpentuls/feat/add-search-places
Browse files Browse the repository at this point in the history
feat(place-search): #29 add search place and show the static map
  • Loading branch information
ronnygunawan authored Oct 24, 2022
2 parents 4a51d88 + 38c4f8d commit 9e9f1c7
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 0 deletions.
67 changes: 67 additions & 0 deletions BotNet.Services/BotCommands/SearchPlace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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<GeoCode>().SearchPlaceAsync(commandArgument);
string staticMapUrl = serviceProvider.GetRequiredService<StaticMap>().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: "<code>Lokasi tidak dapat ditemukan</code>",
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: "<code>Silakan masukkan lokasi di depan perintah /map</code>",
parseMode: ParseMode.Html,
replyToMessageId: message.MessageId,
cancellationToken: cancellationToken);
}
} else {
await telegramBotClient.SendTextMessageAsync(
chatId: message.Chat.Id,
text: "<code>Silakan masukkan lokasi di depan perintah /map</code>",
parseMode: ParseMode.Html,
replyToMessageId: message.MessageId,
cancellationToken: cancellationToken);
}
}
}
}
85 changes: 85 additions & 0 deletions BotNet.Services/GoogleMap/GeoCode.cs
Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// This class intended to get geocoding from address.
/// </summary>
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<GoogleMapOptions> options) {
_apiKey = options.Value.ApiKey;
_httpClientHandler = new();
_httpClient = new(_httpClientHandler);
}

/// <summary>
/// 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]
/// </summary>
/// <param name="place">Place or address that you want to search</param>
/// <returns>strings of coordinates</returns>
/// <exception cref="HttpRequestException"></exception>
public async Task<string> 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<Response>(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;
}
}
}
5 changes: 5 additions & 0 deletions BotNet.Services/GoogleMap/GoogleMapOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BotNet.Services.GoogleMap {
public class GoogleMapOptions {
public string? ApiKey { get; set; }
}
}
13 changes: 13 additions & 0 deletions BotNet.Services/GoogleMap/Models/Geometry.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
}
8 changes: 8 additions & 0 deletions BotNet.Services/GoogleMap/Models/LocationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BotNet.Services.GoogleMap.Models {
public enum LocationType {
ROOFTOP,
RANGE_INTERPOLATED,
GEOMETRIC_CENTER,
APPROXIMATE
}
}
12 changes: 12 additions & 0 deletions BotNet.Services/GoogleMap/Models/Response.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;

namespace BotNet.Services.GoogleMap.Models {

/// <summary>
/// Response for google map api geocoding
/// </summary>
public class Response {
public List<Result>? Results { get; set; }
public string? Status { get; set; }
}
}
8 changes: 8 additions & 0 deletions BotNet.Services/GoogleMap/Models/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BotNet.Services.GoogleMap.Models {

public class Result {
public string? Formatted_Address { get; set; }

public Geometry? Geometry { get; set; }
}
}
11 changes: 11 additions & 0 deletions BotNet.Services/GoogleMap/Models/StatusCode.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions BotNet.Services/GoogleMap/Models/ZoomLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace BotNet.Services.GoogleMap.Models {
public enum ZoomLevel {
World = 1,
LandMass = 5,
City = 10,
Streets = 15,
Buildings = 20
}
}
12 changes: 12 additions & 0 deletions BotNet.Services/GoogleMap/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<GeoCode>();
services.AddTransient<StaticMap>();

return services;
}
}
}
41 changes: 41 additions & 0 deletions BotNet.Services/GoogleMap/StaticMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using BotNet.Services.GoogleMap.Models;
using Microsoft.Extensions.Options;

namespace BotNet.Services.GoogleMap {

/// <summary>
/// Get static map image from google map api
/// </summary>
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<GoogleMapOptions> options) {
_apiKey = options.Value.ApiKey;
}

/// <summary>
/// Get static map image from google map api
/// </summary>
/// <param name="place">Place or address that you want to search</param>
/// <returns>string of url</returns>
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();
}
}
}
3 changes: 3 additions & 0 deletions BotNet/Bot/UpdateHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ await botClient.SendTextMessageAsync(
case "/pse":
await Services.BotCommands.PSE.SearchAsync(botClient, _serviceProvider, update.Message, cancellationToken);
break;
case "/map":
await SearchPlace.SearchPlaceAsync(botClient, _serviceProvider, update.Message, cancellationToken);
break;
}
}
break;
Expand Down
3 changes: 3 additions & 0 deletions BotNet/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,6 +47,7 @@
services.Configure<PistonOptions>(configuration.GetSection("PistonOptions"));
services.Configure<OpenAIOptions>(configuration.GetSection("OpenAIOptions"));
services.Configure<StabilityOptions>(configuration.GetSection("StabilityOptions"));
services.Configure<GoogleMapOptions>(configuration.GetSection("GoogleMapOptions"));
services.AddHttpClient();
services.AddTenorClient();
services.AddFontService();
Expand All @@ -64,6 +66,7 @@
services.AddCraiyonClient();
services.AddStabilityClient();
services.AddTokopediaServices();
services.AddGoogleMaps();

// Hosted Services
services.Configure<BotOptions>(configuration.GetSection("BotOptions"));
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Telegram Bot written in .NET
```json
{
"BotOptions:AccessToken": "yourtoken",
"GoogleMapOptions:ApiKey": "yourApiKey",
"HostingOptions:UseLongPolling": true
}
```
Expand Down

0 comments on commit 9e9f1c7

Please sign in to comment.