Last active
July 25, 2023 15:04
-
-
Save CaCTuCaTu4ECKuu/b11c32508cf5c1fe1942bba3b6997af9 to your computer and use it in GitHub Desktop.
YC cb to queue function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Function | |
{ | |
public class ConfirmationKeyProvider | |
{ | |
private readonly Dictionary<long, string> GroupKeys; | |
public ConfirmationKeyProvider() | |
{ | |
GroupKeys = new Dictionary<long, string>() | |
{ | |
{ 1 , "xxx" }, | |
{ 2, "yyy" } | |
}; | |
} | |
public string GetConfirmationKey(long groupId) | |
{ | |
if (GroupKeys.ContainsKey(groupId)) | |
return GroupKeys[groupId]; | |
return null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net6.0</TargetFramework> | |
<OutputType>Exe</OutputType> | |
<ImplicitUsings>enable</ImplicitUsings> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="AWSSDK.SQS" Version="3.7.200" /> | |
<PackageReference Include="Yandex.Cloud.SDK" Version="1.2.0" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text.Json; | |
using System.Diagnostics; | |
using Yandex.Cloud.Functions; | |
using Amazon.SQS; | |
using Amazon.SQS.Model; | |
namespace Function; | |
public class Handler | |
{ | |
// Environment variables | |
public const string EV_SECRET_KEY_ID = "SECRET_KEY_ID"; | |
public const string EV_SECRET_KEY = "SECRET_KEY"; | |
public const string EV_QUEUE_ENDPOINT = "QUEUE_ENDPOINT"; | |
public const string EV_QUEUE_MSG_GROUP_ID = "QUEUE_MESSAGE_GROUP_ID"; | |
public const string EV_SENDMESSAGE_TIMEOUT = "QUEUE_SENDMESSAGE_TIMEOUT"; | |
// Queue | |
public const string QUEUE_SERVICE_URL = "https://message-queue.api.cloud.yandex.net"; | |
public const string QUEUE_REGION = "ru-central1"; | |
public const string QUEUE_MSG_GROUP_ID_DEFAULT = "callback"; | |
public const int SENDMESSAGE_TIMEOUT_DEFAULT = 350; | |
public const int SENDMESSAGE_RETRIES_DEFAULT = 3; | |
// VK | |
public const string SUCCESS_RESPONSE = "ok"; | |
// Config | |
public string SECRET_KEY_ID => Environment.GetEnvironmentVariable(EV_SECRET_KEY_ID); | |
public string SECRET_KEY => Environment.GetEnvironmentVariable(EV_SECRET_KEY); | |
public string QUEUE_ENDPOINT => Environment.GetEnvironmentVariable(EV_QUEUE_ENDPOINT); | |
private string _msg_group_id = null; | |
private int? _sendmessage_timeout = null; | |
public string MESSAGE_GROUP_ID => _msg_group_id ?? QUEUE_MSG_GROUP_ID_DEFAULT; | |
public int SENDMESSAGE_TIMEOUT => _sendmessage_timeout ?? SENDMESSAGE_TIMEOUT_DEFAULT; | |
public readonly AmazonSQSConfig _clientConfig; | |
public readonly AmazonSQSClient _client; | |
public Handler() | |
{ | |
RequireEnvironmentVariable(EV_SECRET_KEY_ID); | |
RequireEnvironmentVariable(EV_SECRET_KEY); | |
RequireEnvironmentVariable(EV_QUEUE_ENDPOINT); | |
_msg_group_id = Environment.GetEnvironmentVariable(EV_QUEUE_ENDPOINT); | |
if (string.IsNullOrWhiteSpace(_msg_group_id)) | |
_msg_group_id = null; | |
if (int.TryParse(Environment.GetEnvironmentVariable(EV_QUEUE_ENDPOINT), out int sendMsgTimeout)) | |
_sendmessage_timeout = sendMsgTimeout; | |
_clientConfig = new AmazonSQSConfig() | |
{ | |
ServiceURL = QUEUE_SERVICE_URL, | |
AuthenticationRegion = QUEUE_REGION | |
}; | |
_client = new AmazonSQSClient(SECRET_KEY_ID, SECRET_KEY, _clientConfig); | |
} | |
public async Task<Response> FunctionHandler(Request request, Context context) | |
{ | |
VkEvent vkEvent = null; | |
try | |
{ | |
vkEvent = JsonSerializer.Deserialize<VkEvent>(request.body); | |
Logger.LogDebug($"PROCESS RequestID: {context.RequestId} Deserialized successfully", vkEvent.GetExtras()); | |
} | |
catch (Exception ex) | |
{ | |
Logger.LogFatal($"ERROR RequestID: {context.RequestId} Failed to deserialize event {ex.Message}", "exception", ex.GetType().Name); | |
return new Response(400, ex.Message); | |
} | |
if (vkEvent.Type.Equals("message_typing_state", StringComparison.OrdinalIgnoreCase)) | |
{ | |
Logger.LogWarning($"WARNING RequestID: {context.RequestId} Recieved not supported event types - it is recommended to disable sending this type of event"); | |
return new Response(415, "Events of type 'message_typing_state' not supported by this endpoint."); | |
} | |
if (vkEvent.Type.Equals("confirmation", StringComparison.OrdinalIgnoreCase)) | |
return await ConfirmationHandler(vkEvent, context).ConfigureAwait(false); | |
return await HandleSendToQueue(request, vkEvent, context).ConfigureAwait(false); | |
} | |
public Task<Response> ConfirmationHandler(VkEvent vkEvent, Context context) | |
{ | |
var provider = new ConfirmationKeyProvider(); | |
var key = provider.GetConfirmationKey(vkEvent.GroupId); | |
if (key != null) | |
{ | |
Logger.LogWarning("Send confirmation key from memory (may be old)."); | |
return Task.FromResult(new Response(200, key)); | |
} | |
Logger.LogWarning($"ERROR RequestID: {context.RequestId} 'confirmation' events handling currently not implemented"); | |
// TODO: Get confirmation key | |
return Task.FromResult(new Response(200, "sorry")); | |
} | |
public async Task<Response> HandleSendToQueue(Request request, VkEvent vkEvent, Context context) | |
{ | |
var sendMessageRequest = new SendMessageRequest(QUEUE_ENDPOINT, request.body) | |
{ | |
MessageGroupId = MESSAGE_GROUP_ID | |
}; | |
var sendMsgTimeoutCts = new CancellationTokenSource(); | |
sendMsgTimeoutCts.CancelAfter(SENDMESSAGE_TIMEOUT); | |
Stopwatch stopwatch = new Stopwatch(); | |
stopwatch.Start(); | |
int tries = 0; | |
do | |
{ | |
tries++; | |
try | |
{ | |
var sendResult = await _client.SendMessageAsync(sendMessageRequest, sendMsgTimeoutCts.Token) | |
.ConfigureAwait(false); | |
stopwatch.Stop(); | |
if (tries == 1) | |
Logger.LogDebug($"COMPLETE RequestID: {context.RequestId} Sending message to queue takes {(int)stopwatch.Elapsed.TotalMilliseconds} ms.", "q_msg_id", sendResult.MessageId, "q_sequence_num", sendResult.SequenceNumber); | |
else | |
Logger.LogDebug($"COMPLETE RequestID: {context.RequestId} Sending message to queue takes {(int)stopwatch.Elapsed.TotalMilliseconds} ms. and {tries} attempts", "q_msg_id", sendResult.MessageId, "q_sequence_num", sendResult.SequenceNumber); | |
return SuccessRespone(); | |
} | |
catch (Exception ex) | |
{ | |
if (ex is OperationCanceledException) | |
{ | |
if (tries >= SENDMESSAGE_RETRIES_DEFAULT) | |
{ | |
stopwatch.Stop(); | |
Logger.LogFatal($"FAIL RequestID: {context.RequestId} Failed to send message to queue in {(int)stopwatch.Elapsed.TotalMilliseconds} ms. {tries} time(s)", "exception", ex.GetType().Name, "q_msg_timeout", SENDMESSAGE_TIMEOUT.ToString()); | |
return new Response(504, "Processing timeout"); | |
} | |
else | |
Logger.LogWarning($"WARNING RequestID: {context.RequestId} Failed to send message to queue in {(int)stopwatch.Elapsed.TotalMilliseconds} ms. {tries} time(s).", "q_msg_timeout", SENDMESSAGE_TIMEOUT.ToString()); | |
} | |
else | |
{ | |
stopwatch.Stop(); | |
Logger.LogFatal($"FAIL RequestID: {context.RequestId} Failed to send message to queue in {(int)stopwatch.Elapsed.TotalMilliseconds} ms. {ex.Message}", "exception", ex.GetType().Name); | |
return new Response(500, "Error while processing"); | |
} | |
} | |
} | |
while (tries < SENDMESSAGE_RETRIES_DEFAULT); | |
throw new NotImplementedException(); | |
} | |
private static Response SuccessRespone() | |
{ | |
return new Response(200, SUCCESS_RESPONSE); | |
} | |
private void RequireEnvironmentVariable(string key) | |
{ | |
var value = Environment.GetEnvironmentVariable(key); | |
if (string.IsNullOrEmpty(value)) | |
throw new InvalidOperationException($"Function require environment variable of '{key}'."); | |
} | |
public static void Main(string[] args) | |
{ } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Function | |
{ | |
public static class Logger | |
{ | |
public const string LOG_MSG_PATTERN = "{{\"level\":\"{0}\",\"message\":\"{1}\"}}"; | |
public const string LOG_MSG_EXTRA_PATTERN = "{{\"level\":\"{0}\",\"message\":\"{1}\",{2}}}"; | |
public static void Log(string logLevel, string message) | |
{ | |
Console.WriteLine(string.Format(LOG_MSG_PATTERN, logLevel, message)); | |
} | |
public static void Log(string logLevel, string message, Dictionary<string, string> extra) | |
{ | |
if (extra == null || extra.Count == 0) | |
{ | |
Log(logLevel, message); | |
return; | |
} | |
List<string> extras = new List<string>(extra.Count); | |
foreach (var pair in extra) | |
extras.Add($"\"{pair.Key}\":\"{pair.Value}\""); | |
Console.WriteLine(string.Format(LOG_MSG_EXTRA_PATTERN, logLevel, message, string.Join(',', extras))); | |
} | |
public static void Log(string logLevel, string message, params string[] extra) | |
{ | |
if (extra.Length == 0) | |
{ | |
Log(logLevel, message); | |
return; | |
} | |
if (extra.Length % 2 == 0) | |
{ | |
List<string> extras = new List<string>(extra.Length / 2); | |
for (int i = 1; i < extra.Length; i+=2) | |
extras.Add($"\"{extra[i-1]}\":\"{extra[i]}\""); | |
Console.WriteLine(string.Format(LOG_MSG_EXTRA_PATTERN, logLevel, message, string.Join(',', extras))); | |
} | |
else | |
LogError("Count of extra keys and values doesn not match when calling Log(string, string, params string[]) method."); | |
} | |
public static void LogTrace(string message) => Log(LogLevel.TRACE, message); | |
public static void LogDebug(string message) => Log(LogLevel.DEBUG, message); | |
public static void LogInfo(string message) => Log(LogLevel.INFO, message); | |
public static void LogWarning(string message) => Log(LogLevel.WARNING, message); | |
public static void LogError(string message) => Log(LogLevel.ERROR, message); | |
public static void LogFatal(string message) => Log(LogLevel.FATAL, message); | |
public static void LogTrace(string message, Dictionary<string, string> extra) => Log(LogLevel.TRACE, message, extra); | |
public static void LogDebug(string message, Dictionary<string, string> extra) => Log(LogLevel.DEBUG, message, extra); | |
public static void LogInfo(string message, Dictionary<string, string> extra) => Log(LogLevel.INFO, message, extra); | |
public static void LogWarning(string message, Dictionary<string, string> extra) => Log(LogLevel.WARNING, message, extra); | |
public static void LogError(string message, Dictionary<string, string> extra) => Log(LogLevel.ERROR, message, extra); | |
public static void LogFatal(string message, Dictionary<string, string> extra) => Log(LogLevel.FATAL, message, extra); | |
public static void LogTrace(string message, params string[] extra) => Log(LogLevel.TRACE, message, extra); | |
public static void LogDebug(string message, params string[] extra) => Log(LogLevel.DEBUG, message, extra); | |
public static void LogInfo(string message, params string[] extra) => Log(LogLevel.INFO, message, extra); | |
public static void LogWarning(string message, params string[] extra) => Log(LogLevel.WARNING, message, extra); | |
public static void LogError(string message, params string[] extra) => Log(LogLevel.ERROR, message, extra); | |
public static void LogFatal(string message, params string[] extra) => Log(LogLevel.FATAL, message, extra); | |
} | |
public static class LogLevel | |
{ | |
public const string TRACE = "TRACE"; | |
public const string DEBUG = "DEBUG"; | |
public const string INFO = "INFO"; | |
public const string WARNING = "WARN"; | |
public const string ERROR = "ERROR"; | |
public const string FATAL = "FATAL"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Function | |
{ | |
public class Request | |
{ | |
public String httpMethod { get; set; } | |
public String body { get; set; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Function | |
{ | |
public class Response | |
{ | |
public int statusCode { get; set; } | |
public String body { get; set; } | |
public Response(int statusCode, String body) | |
{ | |
this.statusCode = statusCode; | |
this.body = body; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.ComponentModel.DataAnnotations; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
namespace Function | |
{ | |
public class VkEvent | |
{ | |
[JsonPropertyName("type")] | |
[Required] | |
public string Type { get; set; } | |
[JsonPropertyName("event_id")] | |
public string EventId { get; set; } | |
[JsonPropertyName("v")] | |
public string ApiVersion { get; set; } | |
[JsonPropertyName("object")] | |
public JsonElement ObjectJson { get; set; } | |
[JsonPropertyName("group_id")] | |
[Required] | |
public long GroupId { get; set; } | |
/// <summary> | |
/// Ключ доступа, передающийся вместе с запросом от ВК | |
/// </summary> | |
[JsonPropertyName("secret")] | |
public string Secret { get; set; } | |
public Dictionary<string, string> GetExtras(string prefix = "vk_") | |
{ | |
var res = new Dictionary<string, string>(5) | |
{ | |
{ $"{prefix}group_id", GroupId.ToString() }, | |
{ $"{prefix}type", Type }, | |
{ $"{prefix}event_id", EventId }, | |
{ $"{prefix}api_v", string.IsNullOrEmpty(ApiVersion) ? "unknown" : ApiVersion } | |
}; | |
if (!string.IsNullOrEmpty(Secret)) | |
res.Add($"{prefix}secret", Secret); | |
return res; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment