Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(place-search): #29 add search place and show the static map #51

Merged
merged 4 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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