Skip to content

Instantly share code, notes, and snippets.

@CaCTuCaTu4ECKuu
Last active July 25, 2023 15:04
Show Gist options
  • Save CaCTuCaTu4ECKuu/b11c32508cf5c1fe1942bba3b6997af9 to your computer and use it in GitHub Desktop.
Save CaCTuCaTu4ECKuu/b11c32508cf5c1fe1942bba3b6997af9 to your computer and use it in GitHub Desktop.
YC cb to queue function
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;
}
}
}
<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>
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)
{ }
}
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";
}
}
namespace Function
{
public class Request
{
public String httpMethod { get; set; }
public String body { get; set; }
}
}
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;
}
}
}
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