dotnet/space-translate/SpaceTranslate/WebHook/SpaceTranslateWebHookHandler.HandleMenuAction.cs (169 lines of code) (raw):

using JetBrains.Space.Client; using JetBrains.Space.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using SpaceTranslate.Database; namespace SpaceTranslate.WebHook; // TODO comments everywhere public partial class SpaceTranslateWebHookHandler { public override async Task<AppUserActionExecutionResult> HandleMenuActionAsync(MenuActionPayload payload) { using var loggerScopeForClientId = _logger.BeginScope("ClientId={ClientId}", payload.ClientId); var organization = await _db.Organizations.FirstOrDefaultAsync(it => it.ClientId == payload.ClientId); if (organization == null) { _logger.LogWarning("The organization does not exist"); return AppUserActionExecutionResult.Failure("The organization does not exist."); } using var loggerScopeForUserId = _logger.BeginScope("UserId={UserId}", payload.UserId); if (payload.Context is not ChannelMessageMenuActionContext actionContext) { _logger.LogWarning("Unknown payload context type. ContextType={ContextType}", payload.Context?.GetType()); return AppUserActionExecutionResult.Failure("The payload could not be processed."); } if (actionContext.ChannelIdentifier is not ChannelIdentifier.ChannelIdentifierId channelIdentifierId) { _logger.LogWarning("Unknown channel identifier type. ChannelIdentifierType={ChannelIdentifierType}", actionContext.ChannelIdentifier.GetType().Name); return AppUserActionExecutionResult.Failure("The payload could not be processed."); } var user = await _db.Users .Include(m => m.Organization) .FirstOrDefaultAsync(it => it.OrganizationId == organization.Id && it.UserId == payload.UserId); if (user == null) { _logger.LogWarning("User-specific permissions required (no cached credential)"); return PermissionsRequired(null, channelIdentifierId); } var organizationConnection = organization.CreateConnection(); var userConnection = user.CreateConnection(); if (userConnection == null) { _logger.LogWarning("User-specific permissions required (no cached credential)"); return PermissionsRequired(user.Scope, null); } var organizationChatClient = new ChatClient(organizationConnection); var userChatClient = new ChatClient(userConnection); ChannelItemRecord? originalMessage = null; M2ChannelRecord? originalMessageChannelInfo = null; try { originalMessage = await userChatClient.Messages.GetMessageAsync( actionContext.MessageIdentifier, actionContext.ChannelIdentifier, _ => _ .WithAllFieldsWildcard()); // Try accessing text, if it is not accessible we need additional permissions var _ = originalMessage.Text; originalMessageChannelInfo = await userChatClient.Channels.GetChannelAsync(actionContext.ChannelIdentifier); } catch (PermissionDeniedException) { _logger.LogWarning("User-specific permissions required (permission denied)"); return PermissionsRequired(user.Scope, channelIdentifierId); } catch (RefreshTokenRevokedException) { _logger.LogWarning("User-specific permissions required (refresh token revoked)"); return PermissionsRequired(user.Scope, channelIdentifierId); } catch (PropertyNotRequestedException) { _logger.LogWarning("User-specific permissions required (property not accessible)"); return PermissionsRequired(user.Scope, channelIdentifierId); } if (userConnection.AuthenticationTokens?.RefreshToken != null && userConnection.AuthenticationTokens.RefreshToken != user.RefreshToken) { user.RefreshToken = userConnection.AuthenticationTokens.RefreshToken; await _db.SaveChangesAsync(); } var cacheKey = organization.Id + "__" + actionContext.ChannelIdentifier + "__" + actionContext.MessageIdentifier + "__" + originalMessage.Text.ToMd5(); var cachedTranslation = await _cache.GetOrCreateAsync(cacheKey, async entry => { if (string.IsNullOrWhiteSpace(originalMessage.Text)) return "(empty)"; _logger.LogInformation("Requesting translation from DeepL..."); var translatedText = await _translator.TranslateTextAsync( text: originalMessage.Text, sourceLanguageCode: null, targetLanguageCode: "en-US", options: null); _logger.LogInformation("Received translation from DeepL. DetectedSourceLanguageCode={DetectedSourceLanguageCode}", translatedText.DetectedSourceLanguageCode); return translatedText.Text; }); if (cachedTranslation != null) { var channelInfoName = "original message"; if (originalMessageChannelInfo.Contact.Ext is M2SharedChannelContent channelContent) { channelInfoName = $"original message in #{channelContent.Name}"; } await organizationChatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block(new List<MessageSectionElement> { MessageSectionElement.MessageSection(new List<MessageBlockElement> { MessageBlockElement.MessageText( $"Translation of [{channelInfoName}]({organization.ServerUrl}/im/translated/?message={actionContext.MessageIdentifier.ToString()!.Replace("id:", "").Replace("externalId:", "")}&channel={actionContext.ChannelIdentifier.ToString()!.Replace("id:", "")}): "), MessageBlockElement.MessageDivider(), MessageBlockElement.MessageText(cachedTranslation) }) })); } else { _logger.LogWarning("Could not translate message"); return AppUserActionExecutionResult.Failure("Could not translate message."); } return await base.HandleMenuActionAsync(payload); } private AppUserActionExecutionResult PermissionsRequired( string? existingScope, ChannelIdentifier.ChannelIdentifierId? channelIdentifier) { // Always request the global scopes var permissionScopeElements = new HashSet<PermissionScopeElement> { new(PermissionContextIdentifier.Global, PermissionIdentifier.ViewMessages), new(PermissionContextIdentifier.Global, PermissionIdentifier.ViewChannelInfo), new(PermissionContextIdentifier.Global, PermissionIdentifier.ViewDirectMessages) }; // For private channels, channel-specific scopes are needed if (channelIdentifier != null) { permissionScopeElements.Add(new(PermissionContextIdentifier.Channel(channelIdentifier.Id), PermissionIdentifier.ViewMessages)); permissionScopeElements.Add(new(PermissionContextIdentifier.Channel(channelIdentifier.Id), PermissionIdentifier.ViewChannelInfo)); } return AppUserActionExecutionResult.AuthCodeFlowRequired( new List<AuthCodeFlowPermissionsRequest> { new (new PermissionScope(existingScope ?? "") + PermissionScopeBuilder.FromElements(permissionScopeElements), purpose: "translate chat message") }); } public override async Task<ApplicationExecutionResult> HandleRefreshTokenAsync(RefreshTokenPayload payload) { var organization = await _db.Organizations.FirstOrDefaultAsync(it => it.ClientId == payload.ClientId); if (organization == null) { _logger.LogWarning("The organization does not exist. ClientId={ClientId}", payload.ClientId); return new ApplicationExecutionResult("The organization does not exist.", 400); } var user = await _db.Users .Include(m => m.Organization) .FirstOrDefaultAsync(it => it.OrganizationId == organization.Id && it.UserId == payload.UserId); if (user == null) { user = new User { OrganizationId = organization.Id, UserId = payload.UserId, Created = DateTimeOffset.UtcNow }; _db.Users.Add(user); } user.Scope = payload.Scope; user.RefreshToken = payload.RefreshToken; await _db.SaveChangesAsync(); return await base.HandleRefreshTokenAsync(payload); } }