diff --git a/.github/workflows/package.navigator.yml b/.github/workflows/package.navigator.yml new file mode 100644 index 0000000..96b549e --- /dev/null +++ b/.github/workflows/package.navigator.yml @@ -0,0 +1,80 @@ +name: Build & Publish - Navigator +on: + push: + pull_request: + release: + types: + - published +env: + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + # Project name to pack and publish + PROJECT_NAME: Navigator +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + - name: Restore + working-directory: ./src + run: dotnet restore + - name: Build + working-directory: ./src + run: dotnet build -c Release --no-restore + - name: Test + working-directory: ./src + run: dotnet test -c Release + - name: Pack + if: matrix.os == 'ubuntu-latest' + run: dotnet pack -v normal -c Release --no-restore --include-symbols --include-source -p:PackageVersion=$GITHUB_RUN_ID src/$PROJECT_NAME.sln + # - name: Upload Artifact + # if: matrix.os == 'ubuntu-latest' + # uses: actions/upload-artifact@v2 + # with: + # name: nupkg + # path: ./src/${{ env.PROJECT_NAME }}/bin/Release/*.nupkg + publish: + needs: build + if: github.event_name == 'release' + env: + # GitHub Packages Feed settings + GITHUB_FEED: https://nuget.pkg.github.com/navigatorframework/ + GITHUB_USER: navigatorframeworkbot + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Official NuGet Feed settings + NUGET_FEED: https://api.nuget.org/v3/index.json + NUGET_KEY: ${{ secrets.NUGET_KEY }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + - name: Create Release NuGet package + run: | + arrTag=(${GITHUB_REF//\// }) + VERSION="${arrTag[2]}" + echo Version: $VERSION + VERSION="${VERSION//v}" + echo Clean Version: $VERSION + dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME.sln + - name: Push to GitHub Feed + run: | + for f in ./nupkg/*.nupkg + do + curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED + done + - name: Push to NuGet Feed + run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY diff --git a/src/Navigator.Abstractions.Extensions/INavigatorContextExtensions.cs b/src/Navigator.Abstractions.Extensions/INavigatorContextExtensions.cs deleted file mode 100644 index 9d1c902..0000000 --- a/src/Navigator.Abstractions.Extensions/INavigatorContextExtensions.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -using Telegram.Bot.Types.Payments; - -namespace Navigator.Abstractions.Extensions -{ - /// - /// Useful extensions for Navigator Context. - /// - public static class INavigatorContextExtensions - { - #region User - - /// - /// Get a Telegram user. - /// - /// - /// When user is not found. - /// - public static User GetTelegramUser(this INavigatorContext ctx) - { - var user = ctx.GetTelegramUserOrDefault(); - - return user ?? throw new Exception("User not found in update."); - } - - /// - /// Get a Telegram user or default if not found. - /// - /// - /// - public static User? GetTelegramUserOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type switch - { - UpdateType.Message => ctx.Update.Message.From, - UpdateType.InlineQuery => ctx.Update.InlineQuery.From, - UpdateType.ChosenInlineResult => ctx.Update.ChosenInlineResult.From, - UpdateType.CallbackQuery => ctx.Update.CallbackQuery.From, - UpdateType.EditedMessage => ctx.Update.EditedMessage.From, - UpdateType.ChannelPost => ctx.Update.ChannelPost.From, - UpdateType.EditedChannelPost => ctx.Update.EditedChannelPost.From, - UpdateType.ShippingQuery => ctx.Update.ShippingQuery.From, - UpdateType.PreCheckoutQuery => ctx.Update.PreCheckoutQuery.From, - _ => default - }; - } - - #endregion - - #region Chat - - /// - /// Get a Telegram chat. - /// - /// - /// When chat is not found. - /// - public static Chat GetTelegramChat(this INavigatorContext ctx) - { - var chat = ctx.GetTelegramChatOrDefault(); - - return chat ?? throw new Exception("Chat not found in update."); - } - - /// - /// Get a Telegram chat or default if not found. - /// - /// - /// Telegram Chat - public static Chat? GetTelegramChatOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type switch - { - UpdateType.CallbackQuery => ctx.Update.CallbackQuery.Message.Chat, - UpdateType.Message => ctx.Update.Message.Chat, - UpdateType.EditedMessage => ctx.Update.EditedMessage.Chat, - UpdateType.ChannelPost => ctx.Update.ChannelPost.Chat, - UpdateType.EditedChannelPost => ctx.Update.EditedChannelPost.Chat, - _ => default - }; - } - - #endregion - - #region Message - - /// - /// Get a Telegram message. - /// - /// - /// When message is not found. - /// - public static Message GetMessage(this INavigatorContext ctx) - { - return ctx.GetMessageOrDefault() ?? throw new Exception("Message not found in update."); - } - - /// - /// Get a Telegram message or default if not found. - /// - /// - /// - public static Message? GetMessageOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.Message ? ctx.Update.Message : default; - } - - #endregion - - #region InlineQuery - - /// - /// Get a Telegram inline query - /// - /// - /// When inline query is not found. - /// - public static InlineQuery GetInlineQuery(this INavigatorContext ctx) - { - return ctx.GetInlineQueryOrDefault() ?? throw new Exception("InlineQuery not found in update."); - } - - /// - /// Get a Telegram inline query or default if not found. - /// - /// - /// - public static InlineQuery? GetInlineQueryOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.InlineQuery ? ctx.Update.InlineQuery : default; - } - - #endregion - - #region ChosenInlineResult - - public static ChosenInlineResult GetChosenInlineResult(this INavigatorContext ctx) - { - return ctx.GetChosenInlineResultOrDefault() ?? throw new Exception("ChosenInlineResult not found in update."); - } - - public static ChosenInlineResult? GetChosenInlineResultOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.ChosenInlineResult ? ctx.Update.ChosenInlineResult : default; - } - - #endregion - - #region CallbackQuery - - public static CallbackQuery GetCallbackQuery(this INavigatorContext ctx) - { - return ctx.GetCallbackQueryOrDefault() ?? throw new Exception("CallbackQuery not found in update."); - } - - public static CallbackQuery? GetCallbackQueryOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.CallbackQuery ? ctx.Update.CallbackQuery : default; - } - - #endregion - - #region EditedMessage - - public static Message GetEditedMessage(this INavigatorContext ctx) - { - return ctx.GetEditedMessageOrDefault() ?? throw new Exception("EditedMessage not found in update."); - } - - public static Message? GetEditedMessageOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.EditedMessage ? ctx.Update.EditedMessage : default; - } - - #endregion - - #region ChannelPost - - public static Message GetChannelPost(this INavigatorContext ctx) - { - return ctx.GetChannelPostOrDefault() ?? throw new Exception("ChannelPost not found in update."); - } - - public static Message? GetChannelPostOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.ChannelPost ? ctx.Update.ChannelPost : default; - } - - #endregion - - #region EditedChannelPost - - public static Message GetEditedChannelPost(this INavigatorContext ctx) - { - return ctx.GetEditedChannelPostOrDefault() ?? throw new Exception("EditedChannelPost not found in update."); - } - - public static Message? GetEditedChannelPostOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.EditedChannelPost ? ctx.Update.EditedChannelPost : default; - } - - #endregion - - #region ShippingQuery - - public static ShippingQuery GetShippingQuery(this INavigatorContext ctx) - { - return ctx.GetShippingQueryOrDefault() ?? throw new Exception("ShippingQuery not found in update."); - } - - public static ShippingQuery? GetShippingQueryOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.ShippingQuery ? ctx.Update.ShippingQuery : default; - } - - #endregion - - #region PreCheckoutQuery - - public static PreCheckoutQuery GetPreCheckoutQuery(this INavigatorContext ctx) - { - return ctx.GetPreCheckoutQueryOrDefault() ?? throw new Exception("PreCheckoutQuery not found in update."); - } - - public static PreCheckoutQuery? GetPreCheckoutQueryOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.PreCheckoutQuery ? ctx.Update.PreCheckoutQuery : default; - } - - #endregion - - #region Poll - - public static Poll GetPoll(this INavigatorContext ctx) - { - return ctx.GetPollOrDefault() ?? throw new Exception("Poll not found in update."); - } - - public static Poll? GetPollOrDefault(this INavigatorContext ctx) - { - return ctx.Update.Type == UpdateType.Poll ? ctx.Update.Poll : default; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions.Extensions/Navigator.Abstractions.Extensions.csproj b/src/Navigator.Abstractions.Extensions/Navigator.Abstractions.Extensions.csproj deleted file mode 100644 index 4c53843..0000000 --- a/src/Navigator.Abstractions.Extensions/Navigator.Abstractions.Extensions.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net5.0 - enable - true - true - 0.9.99-beta - Navigator.Abstractions.Extensions - Navigator Framework Abstractions Extensions - Lucas Maximiliano Marino - A highly opinionated telegram bot framework, mainly based on Telegram.Bot. - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator - true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - - - - - - - - - - - diff --git a/src/Navigator.Abstractions/IAction.cs b/src/Navigator.Abstractions/IAction.cs deleted file mode 100644 index 52ea586..0000000 --- a/src/Navigator.Abstractions/IAction.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using MediatR; - -namespace Navigator.Abstractions -{ - /// - /// Base action. - /// - public interface IAction : IRequest - { - /// - /// Order at which the action must be evaluated, default is 1000. - /// - int Order { get; } - /// - /// Defines the type of action it can handle. See - /// - string Type { get; } - - /// - /// Timestamp at which the action was launched, in UTC. - /// - DateTime Timestamp { get; } - - /// - /// Can be used to populate the action before triggering "CanHandle". - /// - /// - /// - IAction Init(INavigatorContext ctx); - - /// - /// This function must return true when the incoming update can be handled by this action. - /// - /// - /// - bool CanHandle(INavigatorContext ctx); - } - - public static class ActionType - { - public static readonly string Command = "_navigator_command"; - public static readonly string Message = "_navigator_message"; - public static readonly string CallbackQuery = "_navigator_callbackQuery"; - public static readonly string InlineQuery = "_navigator_inlineQuery"; - public static readonly string InlineResultChosen = "_navigator_inlineResultChosen"; - public static readonly string Poll = "_navigator_poll"; - public static readonly string EditedMessage = "_navigator_editedMessage"; - public static readonly string ChannelPost = "_navigator_channelPost"; - public static readonly string EditedChannelPost = "_navigator_editedChannelPost"; - public static readonly string ShippingQuery = "_navigator_shippingQuery"; - public static readonly string PreCheckoutQuery = "_navigator_preCheckoutQuery"; - public static readonly string ChatMembersAdded = "_navigator_message_chatMembersAdded"; - public static readonly string ChatMemberLeft = "_navigator_message_ChatMemberLeft"; - public static readonly string ChatTitleChanged = "_navigator_message_chatTitleChanged"; - public static readonly string ChatPhotoChanged = "_navigator_message_chatPhotoChanged"; - public static readonly string MessagePinned = "_navigator_message_messagePinned"; - public static readonly string ChatPhotoDeleted = "_navigator_message_chatPhotoDeleted"; - public static readonly string GroupCreated = "_navigator_message_groupCreated"; - public static readonly string SupergroupCreated = "_navigator_message_supergroupCreated"; - public static readonly string ChannelCreated = "_navigator_message_channelCreated"; - public static readonly string MigratedToSupergroup = "_navigator_message_migratedToSupergroup"; - public static readonly string MigratedFromGroup = "_navigator_message_migratedFromGroup"; - public static readonly string Unknown = "_navigator_unknown"; - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/IActionLauncher.cs b/src/Navigator.Abstractions/IActionLauncher.cs deleted file mode 100644 index 0d141ba..0000000 --- a/src/Navigator.Abstractions/IActionLauncher.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Telegram.Bot.Types; - -namespace Navigator.Abstractions -{ - public interface IActionLauncher - { - Task Launch(); - IEnumerable GetActions(Update update); - public string? GetActionType(Update update); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/IBotClient.cs b/src/Navigator.Abstractions/IBotClient.cs deleted file mode 100644 index bdd0817..0000000 --- a/src/Navigator.Abstractions/IBotClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Telegram.Bot; - -namespace Navigator.Abstractions -{ - public interface IBotClient : ITelegramBotClient - { - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorContext.cs b/src/Navigator.Abstractions/INavigatorContext.cs deleted file mode 100644 index 44f1898..0000000 --- a/src/Navigator.Abstractions/INavigatorContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Telegram.Bot.Types; - -namespace Navigator.Abstractions -{ - public interface INavigatorContext - { - Dictionary Items { get; } - public IBotClient Client { get; } - public User BotProfile { get; } - public Update Update { get; } - - Task Init(Update update, Dictionary extensions); - TExtension? Get(string extensionKey, bool throwIfNotFound = false); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorContextBuilder.cs b/src/Navigator.Abstractions/INavigatorContextBuilder.cs deleted file mode 100644 index 6bf1369..0000000 --- a/src/Navigator.Abstractions/INavigatorContextBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using Telegram.Bot.Types; - -namespace Navigator.Abstractions -{ - public interface INavigatorContextBuilder - { - Task Build(Update update); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorContextExtensionProvider.cs b/src/Navigator.Abstractions/INavigatorContextExtensionProvider.cs deleted file mode 100644 index 78d09d9..0000000 --- a/src/Navigator.Abstractions/INavigatorContextExtensionProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Telegram.Bot.Types; - -namespace Navigator.Abstractions -{ - public interface INavigatorContextExtensionProvider - { - int Order => 1000; - Task<(string?, object?)> Process(Update update); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorMiddleware.cs b/src/Navigator.Abstractions/INavigatorMiddleware.cs deleted file mode 100644 index 0920ae8..0000000 --- a/src/Navigator.Abstractions/INavigatorMiddleware.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Navigator.Abstractions -{ - /// - /// Middleware for handling telegram updates incoming as webhooks. - /// - public interface INavigatorMiddleware - { - /// - /// Handles an incoming update from telegram. - /// - /// - /// - Task Handle(HttpRequest httpRequest); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorNotification.cs b/src/Navigator.Abstractions/INavigatorNotification.cs deleted file mode 100644 index 0571d65..0000000 --- a/src/Navigator.Abstractions/INavigatorNotification.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using MediatR; - -namespace Navigator.Abstractions -{ - public interface INavigatorNotification : INotification - { - DateTime Timestamp { get; } - int UpdateId { get; } - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INavigatorOptions.cs b/src/Navigator.Abstractions/INavigatorOptions.cs deleted file mode 100644 index 12da1af..0000000 --- a/src/Navigator.Abstractions/INavigatorOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Navigator.Abstractions -{ - /// - /// - /// - public interface INavigatorOptions - { - bool TryRegisterOption(string key, object option); - bool ForceRegisterOption(string key, object option); - TType? RetrieveOption(string key); - } -} \ No newline at end of file diff --git a/src/Navigator.Abstractions/INotificationLauncher.cs b/src/Navigator.Abstractions/INotificationLauncher.cs deleted file mode 100644 index ad88376..0000000 --- a/src/Navigator.Abstractions/INotificationLauncher.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Navigator.Abstractions -{ - public interface INotificationLauncher - { - Task Launch(); - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/Action.cs b/src/Navigator.Extensions.Actions/Action.cs deleted file mode 100644 index c8a15ec..0000000 --- a/src/Navigator.Extensions.Actions/Action.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - /// - public abstract class Action : IAction - { - /// - public virtual int Order => 1000; - - /// - public abstract string Type { get; } - - /// - public DateTime Timestamp { get; } = DateTime.UtcNow; - - /// - public abstract IAction Init(INavigatorContext ctx); - - /// - public abstract bool CanHandle(INavigatorContext ctx); - - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ActionHandler.cs b/src/Navigator.Extensions.Actions/ActionHandler.cs deleted file mode 100644 index 287ec68..0000000 --- a/src/Navigator.Extensions.Actions/ActionHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ActionHandler : IRequestHandler where TAction : IAction - { - public readonly INavigatorContext Ctx; - - public ActionHandler(INavigatorContext ctx) - { - Ctx = ctx; - } - - public abstract Task Handle(TAction request, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/CallbackQueryAction.cs b/src/Navigator.Extensions.Actions/CallbackQueryAction.cs deleted file mode 100644 index c9016b0..0000000 --- a/src/Navigator.Extensions.Actions/CallbackQueryAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class CallbackQueryAction : Action - { - public override string Type => ActionType.CallbackQuery; - - public string Data { get; set; } = string.Empty; - - public bool IsGameQuery { get; set; } - - public override IAction Init(INavigatorContext ctx) - { - Data = ctx.Update.CallbackQuery.Data; - IsGameQuery = ctx.Update.CallbackQuery.IsGameQuery; - - return this; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChannelCreatedAction.cs b/src/Navigator.Extensions.Actions/ChannelCreatedAction.cs deleted file mode 100644 index 63df302..0000000 --- a/src/Navigator.Extensions.Actions/ChannelCreatedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChannelCreatedAction : Action - { - public override string Type => ActionType.ChannelCreated; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChannelPostAction.cs b/src/Navigator.Extensions.Actions/ChannelPostAction.cs deleted file mode 100644 index facd2da..0000000 --- a/src/Navigator.Extensions.Actions/ChannelPostAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChannelPostAction : Action - { - public override string Type => ActionType.ChannelPost; - public DateTime PostTimestamp { get; protected set; } - public int PostId { get; protected set; } - - public override IAction Init(INavigatorContext ctx) - { - PostTimestamp = ctx.Update.ChannelPost.Date; - PostId = ctx.Update.ChannelPost.MessageId; - - return this; - } - - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChatMemberLeftAction.cs b/src/Navigator.Extensions.Actions/ChatMemberLeftAction.cs deleted file mode 100644 index fc64e7b..0000000 --- a/src/Navigator.Extensions.Actions/ChatMemberLeftAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChatMemberLeftAction : Action - { - public override string Type => ActionType.ChatMemberLeft; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChatMembersAddedAction.cs b/src/Navigator.Extensions.Actions/ChatMembersAddedAction.cs deleted file mode 100644 index 0d94879..0000000 --- a/src/Navigator.Extensions.Actions/ChatMembersAddedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChatMembersAddedAction : Action - { - public override string Type => ActionType.ChatMembersAdded; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChatPhotoChangedAction.cs b/src/Navigator.Extensions.Actions/ChatPhotoChangedAction.cs deleted file mode 100644 index 4553806..0000000 --- a/src/Navigator.Extensions.Actions/ChatPhotoChangedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChatPhotoChangedAction : Action - { - public override string Type => ActionType.ChatPhotoChanged; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChatPhotoDeletedAction.cs b/src/Navigator.Extensions.Actions/ChatPhotoDeletedAction.cs deleted file mode 100644 index 16f1aa2..0000000 --- a/src/Navigator.Extensions.Actions/ChatPhotoDeletedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChatPhotoDeletedAction : Action - { - public override string Type => ActionType.ChatPhotoDeleted; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ChatTitleChangedAction.cs b/src/Navigator.Extensions.Actions/ChatTitleChangedAction.cs deleted file mode 100644 index 68dc234..0000000 --- a/src/Navigator.Extensions.Actions/ChatTitleChangedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ChatTitleChangedAction : Action - { - public override string Type => ActionType.ChatTitleChanged; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/CommandAction.cs b/src/Navigator.Extensions.Actions/CommandAction.cs deleted file mode 100644 index 9332a46..0000000 --- a/src/Navigator.Extensions.Actions/CommandAction.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class CommandAction : Action - { - public override string Type => ActionType.Command; - public DateTime MessageTimestamp { get; protected set; } - public int MessageId { get; protected set; } - public int? ReplyToMessageId { get; protected set; } - public string Command { get; set; } = string.Empty; - - public override IAction Init(INavigatorContext ctx) - { - MessageTimestamp = ctx.Update.Message.Date; - MessageId = ctx.Update.Message.MessageId; - ReplyToMessageId = ctx.Update.Message.ReplyToMessage?.MessageId; - - Command = ctx.Update.Message.ExtractCommand(ctx.BotProfile.Username) ?? string.Empty; - - return this; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/EditedChannelPostAction.cs b/src/Navigator.Extensions.Actions/EditedChannelPostAction.cs deleted file mode 100644 index eaca5be..0000000 --- a/src/Navigator.Extensions.Actions/EditedChannelPostAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class EditedChannelPostAction : ChannelPostAction - { - public override string Type => ActionType.EditedChannelPost; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/EditedMessageAction.cs b/src/Navigator.Extensions.Actions/EditedMessageAction.cs deleted file mode 100644 index c938cb8..0000000 --- a/src/Navigator.Extensions.Actions/EditedMessageAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class EditedMessageAction : MessageAction - { - public override string Type => ActionType.EditedMessage; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/GroupCreatedAction.cs b/src/Navigator.Extensions.Actions/GroupCreatedAction.cs deleted file mode 100644 index 9fe4135..0000000 --- a/src/Navigator.Extensions.Actions/GroupCreatedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class GroupCreatedAction : Action - { - public override string Type => ActionType.GroupCreated; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/InlineQueryAction.cs b/src/Navigator.Extensions.Actions/InlineQueryAction.cs deleted file mode 100644 index 709f5f8..0000000 --- a/src/Navigator.Extensions.Actions/InlineQueryAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class InlineQueryAction : Action - { - public override string Type => ActionType.InlineQuery; - public string InlineQueryId { get; protected set; } = string.Empty; - public string Query { get; protected set; } = string.Empty; - public string Offset { get; protected set; } = string.Empty; - - public override IAction Init(INavigatorContext ctx) - { - InlineQueryId = ctx.Update.InlineQuery.Id; - Query = ctx.Update.InlineQuery.Query; - Offset = ctx.Update.InlineQuery.Offset; - - return this; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/InlineResultChosenAction.cs b/src/Navigator.Extensions.Actions/InlineResultChosenAction.cs deleted file mode 100644 index 703a6fa..0000000 --- a/src/Navigator.Extensions.Actions/InlineResultChosenAction.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class InlineResultChosenAction : Action - { - public override string Type => ActionType.InlineResultChosen; - public string ChosenResultId { get; protected set; } = string.Empty; - public string Query { get; protected set; } = string.Empty; - - public override IAction Init(INavigatorContext ctx) - { - ChosenResultId = ctx.Update.ChosenInlineResult.ResultId; - Query = ctx.Update.ChosenInlineResult.Query; - - return this; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/MessageAction.cs b/src/Navigator.Extensions.Actions/MessageAction.cs deleted file mode 100644 index 6141292..0000000 --- a/src/Navigator.Extensions.Actions/MessageAction.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class MessageAction : Action - { - public override string Type => ActionType.Message; - public DateTime MessageTimestamp { get; protected set; } - public int MessageId { get; protected set; } - public int? ReplyToMessageId { get; protected set; } - - public override IAction Init(INavigatorContext ctx) - { - MessageTimestamp = ctx.Update.Message.Date; - MessageId = ctx.Update.Message.MessageId; - ReplyToMessageId = ctx.Update.Message.ReplyToMessage?.MessageId; - - return this; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/MessageExtension.cs b/src/Navigator.Extensions.Actions/MessageExtension.cs deleted file mode 100644 index 9fdde01..0000000 --- a/src/Navigator.Extensions.Actions/MessageExtension.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; - -namespace Navigator.Extensions.Actions -{ - public static class MessageExtension - { - public static string? ExtractCommand(this Message message, string botName) - { - if (message.Entities?.First()?.Type != MessageEntityType.BotCommand) return default; - - var command = message.EntityValues.First(); - - if (!command.Contains('@')) return command; - - if (!command.Contains(botName)) return default; - - command = command.Substring(0, command.IndexOf('@')); - - return command; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/MessagePinnedAction.cs b/src/Navigator.Extensions.Actions/MessagePinnedAction.cs deleted file mode 100644 index fd0d834..0000000 --- a/src/Navigator.Extensions.Actions/MessagePinnedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class MessagePinnedAction : Action - { - public override string Type => ActionType.MessagePinned; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/MigratedFromGroupAction.cs b/src/Navigator.Extensions.Actions/MigratedFromGroupAction.cs deleted file mode 100644 index 3c7e16a..0000000 --- a/src/Navigator.Extensions.Actions/MigratedFromGroupAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class MigratedFromGroupAction : Action - { - public override string Type => ActionType.MigratedFromGroup; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/MigratedToSupergroupAction.cs b/src/Navigator.Extensions.Actions/MigratedToSupergroupAction.cs deleted file mode 100644 index e8734a5..0000000 --- a/src/Navigator.Extensions.Actions/MigratedToSupergroupAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class MigratedToSupergroupAction : Action - { - public override string Type => ActionType.MigratedToSupergroup; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/Navigator.Extensions.Actions.csproj b/src/Navigator.Extensions.Actions/Navigator.Extensions.Actions.csproj deleted file mode 100644 index 17e60b6..0000000 --- a/src/Navigator.Extensions.Actions/Navigator.Extensions.Actions.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net5.0 - 9 - true - True - 0.9.100-beta - Navigator.Extensions.Actions - Navigator Extensions Actions - Lucas Maximiliano Marino - Common actions for Navigator Framework - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator, Actions, Extension - true - Copyright © Lucas Maximiliano Marino 2020 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - enable - - - - - - - - - - - diff --git a/src/Navigator.Extensions.Actions/PollAction.cs b/src/Navigator.Extensions.Actions/PollAction.cs deleted file mode 100644 index 64a2e8b..0000000 --- a/src/Navigator.Extensions.Actions/PollAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class PollAction : Action - { - public override string Type => ActionType.Poll; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/PreCheckoutQueryAction.cs b/src/Navigator.Extensions.Actions/PreCheckoutQueryAction.cs deleted file mode 100644 index 5845f26..0000000 --- a/src/Navigator.Extensions.Actions/PreCheckoutQueryAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class PreCheckoutQueryAction : Action - { - public override string Type => ActionType.PreCheckoutQuery; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/ShippingQueryAction.cs b/src/Navigator.Extensions.Actions/ShippingQueryAction.cs deleted file mode 100644 index 70f3b96..0000000 --- a/src/Navigator.Extensions.Actions/ShippingQueryAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class ShippingQueryAction : Action - { - public override string Type => ActionType.ShippingQuery; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/SupergroupCreatedAction.cs b/src/Navigator.Extensions.Actions/SupergroupCreatedAction.cs deleted file mode 100644 index ab8e3e0..0000000 --- a/src/Navigator.Extensions.Actions/SupergroupCreatedAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class SupergroupCreatedAction : Action - { - public override string Type => ActionType.SupergroupCreated; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Actions/UnknownAction.cs b/src/Navigator.Extensions.Actions/UnknownAction.cs deleted file mode 100644 index 970666a..0000000 --- a/src/Navigator.Extensions.Actions/UnknownAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Navigator.Abstractions; - -namespace Navigator.Extensions.Actions -{ - public abstract class UnknownAction : Action - { - public override string Type => ActionType.Unknown; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard.Abstractions/IBotManagementService.cs b/src/Navigator.Extensions.Shipyard.Abstractions/IBotManagementService.cs deleted file mode 100644 index 61607d2..0000000 --- a/src/Navigator.Extensions.Shipyard.Abstractions/IBotManagementService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Navigator.Extensions.Shipyard.Abstractions.Model; - -namespace Navigator.Extensions.Shipyard.Abstractions -{ - public interface IBotManagementService - { - Task GetBotInfo(CancellationToken cancellationToken = default); - Task GetBotPic(CancellationToken cancellationToken = default); - Task GetBotCommands(CancellationToken cancellationToken = default); - Task UpdateBotCommands(BotCommands botCommands, CancellationToken cancellationToken = default); - - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotCommands.cs b/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotCommands.cs deleted file mode 100644 index dea35e7..0000000 --- a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotCommands.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Navigator.Extensions.Shipyard.Abstractions.Model -{ - /// - /// Bot commands. - /// - public class BotCommands - { - /// - /// List of commands. - /// - public Dictionary Commands { get; init; } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotInfo.cs b/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotInfo.cs deleted file mode 100644 index b79320f..0000000 --- a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Navigator.Extensions.Shipyard.Abstractions.Model -{ - /// - /// Bot information. - /// - public class BotInfo - { - /// - /// Bot id. - /// - public long Id { get; init; } - - /// - /// Bot username. - /// - public string Username { get; init; } - - /// - /// Bot name. - /// - public string Name { get; init; } - - /// - /// Bot permissions. - /// - public BotPermissions Permissions { get; init; } - - /// - /// Bot permissions. - /// - public class BotPermissions - { - /// - /// CanJoinGroups - /// - public bool CanJoinGroups { get; init; } - - /// - /// CanReadAllGroupMessages - /// - public bool CanReadAllGroupMessages { get; init; } - - /// - /// SupportsInlineQueries - /// - public bool SupportsInlineQueries { get; init; } - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotPic.cs b/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotPic.cs deleted file mode 100644 index 310a420..0000000 --- a/src/Navigator.Extensions.Shipyard.Abstractions/Model/BotPic.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; - -namespace Navigator.Extensions.Shipyard.Abstractions.Model -{ - /// - /// Bot Picture. - /// - public class BotPic - { - /// - /// Picture in stream format. - /// - public MemoryStream File { get; init; } - - /// - /// Mime type of the picture. - /// - public string MimeType { get; init; } = "image/png"; - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard.Abstractions/Navigator.Extensions.Shipyard.Abstractions.csproj b/src/Navigator.Extensions.Shipyard.Abstractions/Navigator.Extensions.Shipyard.Abstractions.csproj deleted file mode 100644 index 864fe98..0000000 --- a/src/Navigator.Extensions.Shipyard.Abstractions/Navigator.Extensions.Shipyard.Abstractions.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net5.0 - enable - true - true - 0.9.100-beta - Navigator.Extensions.Shipyard.Abstractions - Navigator Extensions Shipyard Abstractions - Lucas Maximiliano Marino - A highly opinionated telegram bot framework, mainly based on Telegram.Bot. - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator - true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - - - - - - - - - - - diff --git a/src/Navigator.Extensions.Shipyard/BotManagementService.cs b/src/Navigator.Extensions.Shipyard/BotManagementService.cs deleted file mode 100644 index a45ffc4..0000000 --- a/src/Navigator.Extensions.Shipyard/BotManagementService.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Navigator.Extensions.Shipyard.Abstractions; -using Navigator.Extensions.Shipyard.Abstractions.Model; - -namespace Navigator.Extensions.Shipyard -{ - /// - public class BotManagementService : IBotManagementService - { - private readonly ILogger _logger; - private readonly IBotClient _botClient; - - /// - /// Default constructor. - /// - /// - /// - public BotManagementService(ILogger logger, IBotClient botClient) - { - _logger = logger; - _botClient = botClient; - } - - /// - public async Task GetBotInfo(CancellationToken cancellationToken = default) - { - try - { - var info = await _botClient.GetMeAsync(cancellationToken); - - return new BotInfo - { - Id = info.Id, - Username = info.Username, - Name = info.FirstName, - Permissions = new BotInfo.BotPermissions - { - CanJoinGroups = info.CanJoinGroups ?? false, - CanReadAllGroupMessages = info.CanReadAllGroupMessages ?? false, - SupportsInlineQueries = info.SupportsInlineQueries ?? false - } - }; - } - catch (Exception e) - { - _logger.LogError(e, "Unhandled exception requesting bot information."); - - return default; - } - } - - /// - public async Task GetBotPic(CancellationToken cancellationToken = default) - { - try - { - var photos = await _botClient.GetUserProfilePhotosAsync(_botClient.BotId, cancellationToken: cancellationToken); - - if (photos.TotalCount > 0) - { - var botPicId = photos.Photos.FirstOrDefault()? - .OrderByDescending(x => x.FileSize).FirstOrDefault() - ?.FileId; - - if (!string.IsNullOrWhiteSpace(botPicId)) - { - var pictureStream = new MemoryStream(); - await _botClient.GetInfoAndDownloadFileAsync(botPicId, pictureStream, cancellationToken); - - return new BotPic - { - File = pictureStream - }; - } - } - } - catch (Exception e) - { - _logger.LogError(e, "Unhandled exception requesting bot pic."); - } - - return default; - } - - /// - public async Task GetBotCommands(CancellationToken cancellationToken = default) - { - var commands = await _botClient.GetMyCommandsAsync(cancellationToken); - - return new BotCommands - { - Commands = commands.ToDictionary(command => command.Command, command => command.Description) - }; - } - - /// - public Task UpdateBotCommands(BotCommands botCommands, CancellationToken cancellationToken = default) - { - throw new System.NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/Controllers/BotController.cs b/src/Navigator.Extensions.Shipyard/Controllers/BotController.cs deleted file mode 100644 index 389cdd6..0000000 --- a/src/Navigator.Extensions.Shipyard/Controllers/BotController.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Navigator.Extensions.Shipyard.Abstractions; -using Navigator.Extensions.Shipyard.Middleware; - -namespace Navigator.Extensions.Shipyard.Controllers -{ - /// - /// Bot controller. - /// - [Authorize(AuthenticationSchemes = nameof(ShipyardApiKeyAuthenticationHandler))] - [ApiController] - [ApiVersion("1")] - [Route("shipyard/api/v{v:apiVersion}/[controller]")] - public class BotController : ControllerBase - { - private readonly IBotManagementService _botManagementService; - - /// - /// Default constructor. - /// - /// - public BotController(IBotManagementService botManagementService) - { - _botManagementService = botManagementService; - } - - [HttpGet("information")] - public async Task GetBotInformation(CancellationToken cancellationToken) - { - var information = await _botManagementService.GetBotInfo(cancellationToken); - - return Ok(information); - } - - [HttpGet("pic")] - public async Task GetBotPic(CancellationToken cancellationToken) - { - var botPic = await _botManagementService.GetBotPic(cancellationToken); - - if (botPic is not null) - { - return File(botPic.File, botPic.MimeType); - } - return NotFound("BotPic not found"); - } - - [HttpGet("commands")] - public async Task GetBotCommands(CancellationToken cancellationToken) - { - var botCommands = await _botManagementService.GetBotCommands(cancellationToken); - - return Ok(botCommands); - } - - [HttpPut("commands")] - public async Task UpdateBotCommands() - { - throw new NotImplementedException(); - } - - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/Controllers/ConversationController.cs b/src/Navigator.Extensions.Shipyard/Controllers/ConversationController.cs deleted file mode 100644 index 0e0090f..0000000 --- a/src/Navigator.Extensions.Shipyard/Controllers/ConversationController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Navigator.Abstractions; -using Navigator.Extensions.Shipyard.Middleware; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; -using Telegram.Bot.Types.Enums; - -namespace Navigator.Extensions.Shipyard.Controllers -{ - /// - /// Conversation controller. - /// - /// - /// - [Authorize(AuthenticationSchemes = nameof(ShipyardApiKeyAuthenticationHandler))] - [ApiController] - [ApiVersion("1")] - [Route("shipyard/api/v{v:apiVersion}/conversation")] - public class ConversationController : ControllerBase where TUser : User where TChat : Chat - { - private readonly IEntityManager _entityManager; - private readonly IBotClient _botClient; - - /// - /// Default constructor. - /// - /// - /// - public ConversationController(IEntityManager entityManager, IBotClient botClient) - { - _entityManager = entityManager; - _botClient = botClient; - } - - /// - /// Gets all the users. - /// - /// - /// - [HttpGet("user")] - public IActionResult GetUsers(CancellationToken cancellationToken) - { - try - { - var users = _entityManager.FindAllUsersAsync(cancellationToken); - - if (users is not null) - { - return Ok(users); - } - } - catch (Exception) - { - // ignored - } - - return NotFound(); - } - - /// - /// Gets all the chats. - /// - /// - /// - [HttpGet("chat")] - public IActionResult GetChats(CancellationToken cancellationToken) - { - try - { - var chats = _entityManager.FindAllChatsAsync(cancellationToken); - - if (chats is not null) - { - return Ok(chats); - } - } - catch (Exception) - { - // ignored - } - - return NotFound(); - } - - [HttpPost("chat/{chatId}")] - public async Task SendMessage([FromRoute] long chatId, [FromBody] string message, CancellationToken cancellationToken) - { - if (await _entityManager.FindChatAsync(chatId, cancellationToken) is null) - { - return NotFound(); - } - - await _botClient.SendTextMessageAsync(chatId, message, ParseMode.MarkdownV2, cancellationToken: cancellationToken); - - return Ok(); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/GenericControllerFeatureProvider.cs b/src/Navigator.Extensions.Shipyard/GenericControllerFeatureProvider.cs deleted file mode 100644 index 9efcac3..0000000 --- a/src/Navigator.Extensions.Shipyard/GenericControllerFeatureProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Controllers; -using Navigator.Extensions.Shipyard.Controllers; - -namespace Navigator.Extensions.Shipyard -{ - public class GenericControllerFeatureProvider : IApplicationFeatureProvider - { - private Type UserType { get; } - private Type ChatType { get; } - - public GenericControllerFeatureProvider(Type userType, Type chatType) - { - UserType = userType; - ChatType = chatType; - } - - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - // Check to see if there is a "real" controller for this class - if (!feature.Controllers.Any(t => t.GenericTypeParameters.Any(gp => gp == UserType) && t.GenericTypeParameters.Any(gp => gp == ChatType))) - { - // Create a generic controller for this type - var controllerType = typeof(ConversationController<,>).MakeGenericType(UserType, ChatType).GetTypeInfo(); - feature.Controllers.Add(controllerType); - } - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/Middleware/ShipyardApiKeyAuthenticationHandler.cs b/src/Navigator.Extensions.Shipyard/Middleware/ShipyardApiKeyAuthenticationHandler.cs deleted file mode 100644 index 39784db..0000000 --- a/src/Navigator.Extensions.Shipyard/Middleware/ShipyardApiKeyAuthenticationHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Navigator.Extensions.Shipyard.Middleware -{ - internal class ShipyardApiKeyAuthenticationHandler : AuthenticationHandler - { - private readonly NavigatorOptions _navigatorOptions; - - public ShipyardApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, NavigatorOptions navigatorOptions) : base(options, logger, encoder, clock) - { - _navigatorOptions = navigatorOptions; - } - - /// - protected override Task HandleAuthenticateAsync() - { - if (_navigatorOptions.GetShipyardApiKey() is null) - { - return Task.FromResult(AuthenticateResult.Fail("Navigator Shipyard is not configured. Please configure an API KEY.")); - } - - if (Context.Request.Headers.TryGetValue("SHIPYARD-API-KEY", out var headerApiKey) && headerApiKey == _navigatorOptions.GetShipyardApiKey()) - { - const string username = "manager"; - var claims = new[] - { - new Claim(ClaimTypes.NameIdentifier, username, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Options.ClaimsIssuer) - }; - - ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name)); - - AuthenticationTicket ticket = new(principal, Scheme.Name); - return Task.FromResult(AuthenticateResult.Success(ticket)); - } - - return Task.FromResult(AuthenticateResult.Fail("Unauthorized")); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/Navigator.Extensions.Shipyard.csproj b/src/Navigator.Extensions.Shipyard/Navigator.Extensions.Shipyard.csproj deleted file mode 100644 index 1669592..0000000 --- a/src/Navigator.Extensions.Shipyard/Navigator.Extensions.Shipyard.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net5.0 - enable - true - true - 0.9.100-beta - Navigator.Extensions.Shipyard - Navigator Extensions Shipyard - Lucas Maximiliano Marino - A highly opinionated telegram bot framework, mainly based on Telegram.Bot. - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator - true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Navigator.Extensions.Shipyard/NavigatorOptionsCollectionExtensions.cs b/src/Navigator.Extensions.Shipyard/NavigatorOptionsCollectionExtensions.cs deleted file mode 100644 index 66a39fb..0000000 --- a/src/Navigator.Extensions.Shipyard/NavigatorOptionsCollectionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Shipyard -{ - public static class NavigatorOptionsCollectionExtensions - { - #region ShipyardApiKey - - private const string ShipyardApiKeyKey = "_navigator.extensions.shipyard.options.api_key"; - - public static void SetShipyardApiKey(this INavigatorOptions navigatorOptions, string apiKey) - { - navigatorOptions.TryRegisterOption(ShipyardApiKeyKey, apiKey); - } - - public static string? GetShipyardApiKey(this INavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(ShipyardApiKeyKey); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Shipyard/NavigatorServiceCollectionExtensions.cs b/src/Navigator.Extensions.Shipyard/NavigatorServiceCollectionExtensions.cs deleted file mode 100644 index 3c9b797..0000000 --- a/src/Navigator.Extensions.Shipyard/NavigatorServiceCollectionExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Linq; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Navigator.Extensions.Shipyard.Abstractions; -using Navigator.Extensions.Shipyard.Middleware; -using Navigator.Extensions.Store; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Shipyard -{ - /// - /// Navigator Shipyard Services - /// - public static class NavigatorServiceCollectionExtensions - { - /// - /// Registers all the necessary navigatorBuilder for shipyard to work. - /// - /// - /// - /// - public static NavigatorBuilder AddShipyard(this NavigatorBuilder navigatorBuilder, Action? options = default) - { - if (options is null) - { - throw new ArgumentException("Please configure shipyard options."); - } - - options.Invoke(navigatorBuilder.Options); - - navigatorBuilder.RegisterOrReplaceOptions(); - - navigatorBuilder.Services.AddControllers().ConfigureApplicationPartManager(m => - m.FeatureProviders.Add( - new GenericControllerFeatureProvider(navigatorBuilder.Options.GetUserType()!, navigatorBuilder.Options.GetChatType()!) - )); - navigatorBuilder.Services.AddAuthentication().AddScheme( - nameof(ShipyardApiKeyAuthenticationHandler), o => { }); - - navigatorBuilder.Services.AddScoped(); - - return navigatorBuilder; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions.Extensions/INavigatorContextExtensions.cs b/src/Navigator.Extensions.Store.Abstractions.Extensions/INavigatorContextExtensions.cs deleted file mode 100644 index 56431c3..0000000 --- a/src/Navigator.Extensions.Store.Abstractions.Extensions/INavigatorContextExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Store.Abstractions.Extensions -{ - public static class INavigatorContextExtensions - { - public static string DefaultUserKey => "navigator.extensions.store.user"; - public static string DefaultChatKey => "navigator.extensions.store.chat"; - - #region User - - public static TUser GetUser(this INavigatorContext ctx) where TUser : User - { - return ctx.GetUserOrDefault() ?? throw new Exception("User not found"); - } - - public static TUser? GetUserOrDefault(this INavigatorContext ctx, TUser defaultValue = default) where TUser : User - { - return ctx.Get(DefaultUserKey) ?? defaultValue; - } - - #endregion - - #region Chat - - public static TChat GetChat(this INavigatorContext ctx) where TChat : Chat - { - return ctx.GetChatOrDefault() ?? throw new Exception("User not found"); - } - - public static TChat? GetChatOrDefault(this INavigatorContext ctx, TChat defaultValue = default) where TChat : Chat - { - return ctx.Get(DefaultChatKey) ?? defaultValue; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions.Extensions/Navigator.Extensions.Store.Abstractions.Extensions.csproj b/src/Navigator.Extensions.Store.Abstractions.Extensions/Navigator.Extensions.Store.Abstractions.Extensions.csproj deleted file mode 100644 index f5b247d..0000000 --- a/src/Navigator.Extensions.Store.Abstractions.Extensions/Navigator.Extensions.Store.Abstractions.Extensions.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net5.0 - enable - true - true - 0.9.99-beta - Navigator.Extensions.Store.Abstractions.Extensions - Navigator Extensions Store Abstractions Extensions - Lucas Maximiliano Marino - Store extension for Navigator Framework. - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator, EF Core, Store, Database, Extension - true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - - - - - - - - - - - diff --git a/src/Navigator.Extensions.Store.Abstractions/Entity/Chat.cs b/src/Navigator.Extensions.Store.Abstractions/Entity/Chat.cs deleted file mode 100644 index e6dc1f4..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/Entity/Chat.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Navigator.Extensions.Store.Abstractions.Entity -{ - public class Chat - { - public Chat() - { - Conversations = new List(); - CreatedAt = DateTime.UtcNow; - } - - /// - /// Id of the chat. - /// - public long Id { get; set; } - /// - /// Optional, available when the type of the chat is private. - /// - public string? Username { get; set; } - /// - /// Optional, available when the type of the chat is a group, supergroup or channel. - /// - public string? Title { get; set; } - /// - /// Type of the chat. - /// - public ChatType Type { get; set; } - /// - /// List of conversations that this chat has. - /// - public List Conversations { get; set; } - /// - /// Date of first interaction with the chat. - /// - public DateTime CreatedAt { get; set; } - - public enum ChatType - { - Private, - Group, - Channel, - Supergroup, - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/Entity/Conversation.cs b/src/Navigator.Extensions.Store.Abstractions/Entity/Conversation.cs deleted file mode 100644 index ff0efd4..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/Entity/Conversation.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Navigator.Extensions.Store.Abstractions.Entity -{ - public class Conversation - { - public Conversation() - { - CreatedAt = DateTime.UtcNow; - } - - public long ChatId { get; set; } - public Chat Chat { get; set; } - public int UserId { get; set; } - public User User { get; set; } - public DateTime CreatedAt { get; set; } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/Entity/User.cs b/src/Navigator.Extensions.Store.Abstractions/Entity/User.cs deleted file mode 100644 index 5f3c025..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/Entity/User.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Navigator.Extensions.Store.Abstractions.Entity -{ - public class User - { - public User() - { - Conversations = new List(); - CreatedAt = DateTime.UtcNow; - } - - /// - /// Id of the user. - /// - public int Id { get; set; } - /// - /// Specifies if the user is a bot or not. - /// - public bool IsBot { get; set; } - /// - /// Language code of the user client. - /// - public string? LanguageCode { get; set; } - /// - /// Username of the user. - /// - public string? Username { get; set; } - /// - /// List of conversations that this user has. - /// - public List Conversations { get; set; } - /// - /// Date of first interaction with the user. - /// - public DateTime CreatedAt { get; set; } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/IChatMapper.cs b/src/Navigator.Extensions.Store.Abstractions/IChatMapper.cs deleted file mode 100644 index 4c7091a..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/IChatMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Telegram.Bot.Types; - -namespace Navigator.Extensions.Store.Abstractions -{ - public interface IChatMapper - { - TChat Parse(Chat chat); - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/IEntityManager.cs b/src/Navigator.Extensions.Store.Abstractions/IEntityManager.cs deleted file mode 100644 index 772eac3..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/IEntityManager.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Store.Abstractions -{ - public interface IEntityManager - where TUser : User - where TChat : Chat - { - Task Handle(Telegram.Bot.Types.User telegramUser, CancellationToken cancellationToken = default); - Task Handle(Telegram.Bot.Types.User telegramUser, Telegram.Bot.Types.Chat telegramChat, CancellationToken cancellationToken = default); - TUser? FindUser(int id); - Task FindUserAsync(int id, CancellationToken cancellationToken = default); - IEnumerable FindAllUsersAsync(CancellationToken cancellationToken = default); - TChat? FindChat(long id); - Task FindChatAsync(long id, CancellationToken cancellationToken = default); - IEnumerable FindAllChatsAsync(CancellationToken cancellationToken = default); - Task MigrateFromGroup(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default); - Task MigrateToSupergroup(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default); - Task ChatTitleChanged(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default); - Task ChatMemberLeft(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default); - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/IUserMapper.cs b/src/Navigator.Extensions.Store.Abstractions/IUserMapper.cs deleted file mode 100644 index dd11aef..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/IUserMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Telegram.Bot.Types; - -namespace Navigator.Extensions.Store.Abstractions -{ - public interface IUserMapper - { - TUser Parse(User user); - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Abstractions/Navigator.Extensions.Store.Abstractions.csproj b/src/Navigator.Extensions.Store.Abstractions/Navigator.Extensions.Store.Abstractions.csproj deleted file mode 100644 index 215aa22..0000000 --- a/src/Navigator.Extensions.Store.Abstractions/Navigator.Extensions.Store.Abstractions.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net5.0 - enable - true - true - 0.9.100-beta - Navigator.Extensions.Store.Abstractions - Navigator Extensions Store Abstractions - Lucas Maximiliano Marino - Store extension for Navigator Framework. - https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE - https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator, EF Core, Store, Database, Extension - true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - - - - - - - - - - - diff --git a/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramChatDataExtractor.cs b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramChatDataExtractor.cs new file mode 100644 index 0000000..da60ac3 --- /dev/null +++ b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramChatDataExtractor.cs @@ -0,0 +1,30 @@ +using Navigator.Entities; +using Navigator.Extensions.Store.Extractors; +using Navigator.Providers.Telegram.Entities; + +namespace Navigator.Extensions.Store.Telegram.Extractors; + +public class TelegramChatDataExtractor : IDataExtractor +{ + public Dictionary From(Conversation source) + { + var dict = new Dictionary(); + + if (source.Chat is TelegramChat chat) + { + dict.Add($"navigator.telegram.{nameof(TelegramChat.ExternalIdentifier)}", chat.ExternalIdentifier.ToString()); + + if (chat.Title is not null) + { + dict.Add($"navigator.telegram.{nameof(TelegramChat.Title)}", chat.Title); + } + } + + return dict; + } + + public bool Maps(Type type) + { + return type == typeof(TelegramChat); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramConversationDataExtractor.cs b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramConversationDataExtractor.cs new file mode 100644 index 0000000..cc301c1 --- /dev/null +++ b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramConversationDataExtractor.cs @@ -0,0 +1,20 @@ +using Navigator.Entities; +using Navigator.Extensions.Store.Extractors; +using Navigator.Providers.Telegram.Entities; + +namespace Navigator.Extensions.Store.Telegram.Extractors; + +public class TelegramConversationDataExtractor : IDataExtractor +{ + public Dictionary From(Conversation source) + { + var dict = new Dictionary(); + + return dict; + } + + public bool Maps(Type type) + { + return type == typeof(TelegramConversation); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramUserDataExtractor.cs b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramUserDataExtractor.cs new file mode 100644 index 0000000..b1ad08b --- /dev/null +++ b/src/Navigator.Extensions.Store.Telegram/Extractors/TelegramUserDataExtractor.cs @@ -0,0 +1,42 @@ +using Navigator.Entities; +using Navigator.Extensions.Store.Extractors; +using Navigator.Providers.Telegram.Entities; + +namespace Navigator.Extensions.Store.Telegram.Extractors; + +public class TelegramUserDataExtractor : IDataExtractor +{ + public Dictionary From(Conversation source) + { + var dict = new Dictionary(); + + if (source.User is TelegramUser user) + { + dict.Add($"navigator.telegram.{nameof(TelegramUser.ExternalIdentifier)}", user.ExternalIdentifier.ToString()); + + if (user.Username is not null) + { + dict.Add($"navigator.telegram.{nameof(TelegramUser.Username)}", user.Username); + } + + dict.Add($"navigator.telegram.{nameof(TelegramUser.FirstName)}", user.FirstName); + + if (user.LastName is not null) + { + dict.Add($"navigator.telegram.{nameof(TelegramUser.LastName)}", user.LastName); + } + + if (user.LanguageCode is not null) + { + dict.Add($"navigator.telegram.{nameof(TelegramUser.LanguageCode)}", user.LanguageCode); + } + } + + return dict; + } + + public bool Maps(Type type) + { + return type == typeof(TelegramUser); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store.Telegram/Navigator.Extensions.Store.Telegram.csproj b/src/Navigator.Extensions.Store.Telegram/Navigator.Extensions.Store.Telegram.csproj new file mode 100644 index 0000000..df0cdb6 --- /dev/null +++ b/src/Navigator.Extensions.Store.Telegram/Navigator.Extensions.Store.Telegram.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + true + enable + true + Navigator.Extensions.Store + Lucas Maximiliano Marino + Telegram provider persistence implementation for Navigator Store Extension. + https://github.com/navigatorframework/navigator + https://github.com/navigatorframework/navigator + https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png + Store, Bot, Telegram, Navigator + true + AGPL-3.0-only + Copyright © Lucas Maximiliano Marino 2022 + + + + + + + + diff --git a/src/Navigator.Extensions.Store.Telegram/NavigatorExtensionConfigurationExtensions.cs b/src/Navigator.Extensions.Store.Telegram/NavigatorExtensionConfigurationExtensions.cs new file mode 100644 index 0000000..6b6445b --- /dev/null +++ b/src/Navigator.Extensions.Store.Telegram/NavigatorExtensionConfigurationExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Navigator.Configuration; +using Navigator.Configuration.Extension; +using Navigator.Extensions.Store.Extractors; +using Navigator.Extensions.Store.Telegram.Extractors; + +namespace Navigator.Extensions.Store.Telegram; + +public static class NavigatorExtensionConfigurationExtensions +{ + public static NavigatorConfiguration StoreForTelegram(this NavigatorExtensionConfiguration extensionConfiguration, Action? dbContextOptions = default) + { + return extensionConfiguration.Extension(configuration => + { + configuration.Services.AddSingleton(); + configuration.Services.AddSingleton(); + configuration.Services.AddSingleton(); + }); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Bundled/Extensions/NavigatorContextExtensions.cs b/src/Navigator.Extensions.Store/Bundled/Extensions/NavigatorContextExtensions.cs new file mode 100644 index 0000000..30bc9d1 --- /dev/null +++ b/src/Navigator.Extensions.Store/Bundled/Extensions/NavigatorContextExtensions.cs @@ -0,0 +1,25 @@ +using Navigator.Context; + +namespace Navigator.Extensions.Store.Bundled.Extensions; + +public static class NavigatorContextExtensions +{ + public static INavigatorStore GetStore(this INavigatorContext navigatorContext) + { + var store = navigatorContext.GetStoreOrDefault(); + + return store ?? throw new NavigatorException("Store was not found."); + } + + public static INavigatorStore? GetStoreOrDefault(this INavigatorContext navigatorContext) + { + var @store = navigatorContext.Extensions.GetValueOrDefault(StoreContextExtension.NavigatorStoreKey); + + if (store is INavigatorStore navigatorStore) + { + return navigatorStore; + } + + return default; + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Bundled/StoreContextExtensions.cs b/src/Navigator.Extensions.Store/Bundled/StoreContextExtensions.cs new file mode 100644 index 0000000..41bf192 --- /dev/null +++ b/src/Navigator.Extensions.Store/Bundled/StoreContextExtensions.cs @@ -0,0 +1,23 @@ +using Navigator.Context; +using Navigator.Context.Extensions; + +namespace Navigator.Extensions.Store.Bundled; + +internal class StoreContextExtension : INavigatorContextExtension +{ + public const string NavigatorStoreKey = "_navigator.extensions.store.navigator_store"; + + private readonly INavigatorStore _navigatorStore; + + public StoreContextExtension(INavigatorStore navigatorStore) + { + _navigatorStore = navigatorStore; + } + + public Task Extend(INavigatorContext navigatorContext, INavigatorContextBuilderOptions builderOptions) + { + navigatorContext.Extensions.TryAdd(NavigatorStoreKey, _navigatorStore); + + return Task.FromResult(navigatorContext); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Bundled/StoreConversationContextExtension.cs b/src/Navigator.Extensions.Store/Bundled/StoreConversationContextExtension.cs new file mode 100644 index 0000000..2f5dde7 --- /dev/null +++ b/src/Navigator.Extensions.Store/Bundled/StoreConversationContextExtension.cs @@ -0,0 +1,119 @@ +using Microsoft.EntityFrameworkCore; +using Navigator.Context; +using Navigator.Context.Extensions; +using Navigator.Extensions.Store.Context; +using Navigator.Extensions.Store.Entities; +using Navigator.Extensions.Store.Extractors; + +namespace Navigator.Extensions.Store.Bundled; + +internal class StoreConversationContextExtension : INavigatorContextExtension +{ + public const string UniversalConversation = "_navigator.extensions.store.source"; + + private readonly NavigatorDbContext _dbContext; + private readonly IEnumerable _dataExtractors; + + public StoreConversationContextExtension(NavigatorDbContext dbContext, IEnumerable dataExtractors) + { + _dbContext = dbContext; + _dataExtractors = dataExtractors; + } + + public async Task Extend(INavigatorContext navigatorContext, INavigatorContextBuilderOptions builderOptions) + { + if (await _dbContext.Conversations.AnyAsync(e => e.Id == navigatorContext.Conversation.Id)) + { + return navigatorContext; + } + + var user = await TryStoreUserAsync(navigatorContext.Conversation); + var chat = await TryStoreChatAsync(navigatorContext.Conversation); + await TryStoreConversationAsync(navigatorContext.Conversation, user, chat); + + await _dbContext.SaveChangesAsync(); + + return navigatorContext; + } + + private async Task TryStoreUserAsync(Navigator.Entities.Conversation source) + { + if (await _dbContext.Users.AnyAsync(user => user.Id == source.User.Id)) return default; + + var user = new User + { + Id = source.User.Id + }; + + var data = _dataExtractors + .FirstOrDefault(extractor => extractor.Maps(source.User.GetType()))? + .From(source); + + if (data is not null) + { + foreach (var pair in data) + { + user.Data.Add(pair); + } + } + + await _dbContext.Users.AddAsync(user); + + return user; + } + + private async Task TryStoreChatAsync(Navigator.Entities.Conversation source) + { + if (source.Chat is null) return default; + + if (await _dbContext.Chats.AnyAsync(chat => chat.Id == source.Chat.Id)) return default; + + var chat = new Chat + { + Id = source.Chat.Id + }; + + var data = _dataExtractors + .FirstOrDefault(extractor => extractor.Maps(source.Chat.GetType()))? + .From(source); + + if (data is not null) + { + foreach (var pair in data) + { + chat.Data.Add(pair); + } + } + + await _dbContext.Chats.AddAsync(chat); + + return chat; + + } + + private async Task TryStoreConversationAsync(Navigator.Entities.Conversation source, User? user, Chat? chat) + { + if (user is null || chat is null) return; + + var conversation = new Conversation(user, chat) + { + Id = source.Id, + User = user, + Chat = chat + }; + + var data = _dataExtractors + .FirstOrDefault(extractor => extractor.Maps(source.GetType()))? + .From(source); + + if (data is not null) + { + foreach (var pair in data) + { + chat.Data.Add(pair); + } + } + + await _dbContext.Conversations.AddAsync(conversation); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Configuration/ChatEntityTypeConfiguration.cs b/src/Navigator.Extensions.Store/Context/Configuration/ChatEntityTypeConfiguration.cs new file mode 100644 index 0000000..696e9df --- /dev/null +++ b/src/Navigator.Extensions.Store/Context/Configuration/ChatEntityTypeConfiguration.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Navigator.Extensions.Store.Entities; + +namespace Navigator.Extensions.Store.Context.Configuration; + +public class ChatEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + builder.HasMany(e => e.Users) + .WithMany(e => e.Chats) + .UsingEntity( + e => e.HasOne(e => e.User) + .WithMany(e => e.Conversations), + e=> e.HasOne(e => e.Chat) + .WithMany(e => e.Conversations)); + + builder.Property(e => e.Data) + .HasConversion( + dictionary => JsonSerializer.Serialize(dictionary, default(JsonSerializerOptions)), + json => JsonSerializer.Deserialize>(json, default(JsonSerializerOptions)) + ?? new Dictionary(), + ValueComparer.CreateDefault(typeof(IDictionary), false)); + + builder.Property(e => e.FirstInteractionAt) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Configuration/ConversationEntityTypeConfiguration.cs b/src/Navigator.Extensions.Store/Context/Configuration/ConversationEntityTypeConfiguration.cs index 0008eed..2193bf9 100644 --- a/src/Navigator.Extensions.Store/Context/Configuration/ConversationEntityTypeConfiguration.cs +++ b/src/Navigator.Extensions.Store/Context/Configuration/ConversationEntityTypeConfiguration.cs @@ -1,24 +1,23 @@ -using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Navigator.Extensions.Store.Abstractions.Entity; +using Navigator.Extensions.Store.Entities; -namespace Navigator.Extensions.Store.Context.Configuration +namespace Navigator.Extensions.Store.Context.Configuration; + +public class ConversationEntityTypeConfiguration : IEntityTypeConfiguration { - /// - public class ConversationEntityTypeConfiguration : IEntityTypeConfiguration + public void Configure(EntityTypeBuilder builder) { - /// - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(e => new {e.ChatId, e.UserId}); - - builder.HasOne(e => e.User) - .WithMany(e => e.Conversations) - .HasForeignKey(e => e.UserId); - - builder.HasOne(e => e.Chat) - .WithMany(e => e.Conversations) - .HasForeignKey(e => e.ChatId); - } + builder.Property(e => e.Data) + .HasConversion( + dictionary => JsonSerializer.Serialize(dictionary, default(JsonSerializerOptions)), + json => JsonSerializer.Deserialize>(json, default(JsonSerializerOptions)) + ?? new Dictionary(), + ValueComparer.CreateDefault(typeof(IDictionary), false)); + + builder.Property(e => e.FirstInteractionAt) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Configuration/UserEntityTypeConfiguration.cs b/src/Navigator.Extensions.Store/Context/Configuration/UserEntityTypeConfiguration.cs new file mode 100644 index 0000000..ddb07c8 --- /dev/null +++ b/src/Navigator.Extensions.Store/Context/Configuration/UserEntityTypeConfiguration.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Navigator.Extensions.Store.Entities; + +namespace Navigator.Extensions.Store.Context.Configuration; + +public class UserEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + builder.HasMany(e => e.Chats) + .WithMany(e => e.Users) + .UsingEntity( + e => e.HasOne(e => e.Chat) + .WithMany(e => e.Conversations), + e=> e.HasOne(e => e.User) + .WithMany(e => e.Conversations)); + + builder.Property(e => e.Data) + .HasConversion( + dictionary => JsonSerializer.Serialize(dictionary, default(JsonSerializerOptions)), + json => JsonSerializer.Deserialize>(json, default(JsonSerializerOptions)) + ?? new Dictionary(), + ValueComparer.CreateDefault(typeof(IDictionary), false)); + + builder.Property(e => e.FirstInteractionAt) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Extension/DbContextOptionsBuilderExtensions.cs b/src/Navigator.Extensions.Store/Context/Extension/DbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000..0bccdb5 --- /dev/null +++ b/src/Navigator.Extensions.Store/Context/Extension/DbContextOptionsBuilderExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Navigator.Extensions.Store.Context.Extension; + +public static class DbContextOptionsBuilderExtensions +{ + public static void UsingStoreExtension(this DbContextOptionsBuilder builder) + where TExtension : class, IDbContextOptionsExtension, new() + { + ((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(new TExtension()); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtension.cs b/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtension.cs new file mode 100644 index 0000000..b739743 --- /dev/null +++ b/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtension.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Navigator.Extensions.Store.Context.Extension; + +public abstract class NavigatorStoreModelExtension : IDbContextOptionsExtension +{ + public Action? Extension { get; protected set; } + public Action? ExtensionServices { get; protected set; } + + public void ApplyServices(IServiceCollection services) + { + + } + + public void Validate(IDbContextOptions options) + { + + } + + public abstract DbContextOptionsExtensionInfo Info { get; } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtensionInfo.cs b/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtensionInfo.cs new file mode 100644 index 0000000..860a17e --- /dev/null +++ b/src/Navigator.Extensions.Store/Context/Extension/NavigatorStoreModelExtensionInfo.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Navigator.Extensions.Store.Context.Extension; + +public abstract class NavigatorStoreModelExtensionInfo : DbContextOptionsExtensionInfo +{ + public NavigatorStoreModelExtensionInfo(IDbContextOptionsExtension extension) : base(extension) + { + } + + public override int GetServiceProviderHashCode() + { + + var hashCode = new HashCode(); + + return hashCode.ToHashCode(); + } + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + { + return other is NavigatorStoreModelExtensionInfo; + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + } + + public override bool IsDatabaseProvider { get; } + public override string LogFragment { get; } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Context/NavigatorDbContext.cs b/src/Navigator.Extensions.Store/Context/NavigatorDbContext.cs index 92ccfab..e079a3f 100644 --- a/src/Navigator.Extensions.Store/Context/NavigatorDbContext.cs +++ b/src/Navigator.Extensions.Store/Context/NavigatorDbContext.cs @@ -1,38 +1,45 @@ -using Microsoft.EntityFrameworkCore; -using Navigator.Extensions.Store.Abstractions.Entity; +using Microsoft.EntityFrameworkCore; using Navigator.Extensions.Store.Context.Configuration; +using Navigator.Extensions.Store.Context.Extension; +using Chat = Navigator.Extensions.Store.Entities.Chat; +using Conversation = Navigator.Extensions.Store.Entities.Conversation; +using User = Navigator.Extensions.Store.Entities.User; -namespace Navigator.Extensions.Store.Context +namespace Navigator.Extensions.Store.Context; + +public class NavigatorDbContext : DbContext { - public class NavigatorDbContext : NavigatorDbContext + public DbSet Users { get; set; } + public DbSet Chats { get; set; } + public DbSet Conversations { get; set; } + + protected NavigatorDbContext() { - public NavigatorDbContext(DbContextOptions options) : base(options) - { - } } - - public class NavigatorDbContext : DbContext - where TUser : User - where TChat : Chat - { - public DbSet Users { get; set; } - public DbSet Chats { get; set; } - public DbSet Conversations { get; set; } - protected NavigatorDbContext() - { - } + private readonly IList> _entityTypeConfigurations = new List>(); - public NavigatorDbContext(DbContextOptions options) : base(options) + public NavigatorDbContext(DbContextOptions options) : base(options) + { + foreach (var extension in options.Extensions + .OfType() + .Select(e => e.Extension)) { + if (extension is not null) _entityTypeConfigurations.Add(extension); } + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); - modelBuilder.ApplyConfiguration(new ConversationEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ChatEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new UserEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new ConversationEntityTypeConfiguration()); + foreach (var entityTypeConfiguration in _entityTypeConfigurations) + { + entityTypeConfiguration.Invoke(modelBuilder); } } } \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/DefaultChatMapper.cs b/src/Navigator.Extensions.Store/DefaultChatMapper.cs deleted file mode 100644 index 5396606..0000000 --- a/src/Navigator.Extensions.Store/DefaultChatMapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Store -{ - public class DefaultChatMapper : IChatMapper - { - public Chat Parse(Telegram.Bot.Types.Chat chat) - { - return new Chat - { - Id = chat.Id, - Username = chat.Username, - Title = chat.Title, - Type = Enum.Parse(chat.Type.ToString()) - }; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/DefaultEntityManager.cs b/src/Navigator.Extensions.Store/DefaultEntityManager.cs deleted file mode 100644 index 128ac1a..0000000 --- a/src/Navigator.Extensions.Store/DefaultEntityManager.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; -using Navigator.Extensions.Store.Context; -using Telegram.Bot.Types; -using Chat = Navigator.Extensions.Store.Abstractions.Entity.Chat; -using User = Navigator.Extensions.Store.Abstractions.Entity.User; - -namespace Navigator.Extensions.Store -{ - public class DefaultEntityManager : IEntityManager where TContext : NavigatorDbContext - where TUser : User - where TChat : Chat - { - private readonly ILogger> _logger; - private readonly TContext _navigatorDbContext; - private readonly IUserMapper _userMapper; - private readonly IChatMapper _chatMapper; - - public DefaultEntityManager(ILogger> logger, TContext navigatorDbContext, - IUserMapper userMapper, IChatMapper chatMapper) - { - _logger = logger; - _navigatorDbContext = navigatorDbContext; - _userMapper = userMapper; - _chatMapper = chatMapper; - } - - public async Task Handle(Telegram.Bot.Types.User telegramUser, CancellationToken cancellationToken = default) - { - _logger.LogDebug("Handling user {@UserId}", telegramUser.Id); - - var user = await _navigatorDbContext.Users.Include(u => u.Conversations) - .FirstOrDefaultAsync(u => u.Id == telegramUser.Id, cancellationToken); - - if (user == null) - { - user = _userMapper.Parse(telegramUser); - - await _navigatorDbContext.AddAsync(user, cancellationToken); - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - } - - public async Task Handle(Telegram.Bot.Types.User telegramUser, Telegram.Bot.Types.Chat telegramChat, - CancellationToken cancellationToken = default) - { - _logger.LogDebug("Handling user {@UserId} and chat {@ChatId}", telegramUser.Id, telegramChat.Id); - - var userIsNew = false; - var chatIsNew = false; - var conversationIsNew = false; - - var user = await _navigatorDbContext.Users.Include(u => u.Conversations) - .FirstOrDefaultAsync(u => u.Id == telegramUser.Id, cancellationToken); - - var chat = await _navigatorDbContext.Chats.Include(u => u.Conversations) - .FirstOrDefaultAsync(u => u.Id == telegramChat.Id, cancellationToken); - - if (user == null) - { - userIsNew = true; - user = _userMapper.Parse(telegramUser); - await _navigatorDbContext.Users.AddAsync(user, cancellationToken); - } - - if (chat == null) - { - chatIsNew = true; - chat = _chatMapper.Parse(telegramChat); - await _navigatorDbContext.Chats.AddAsync(chat, cancellationToken); - } - - if (chat.Conversations.FirstOrDefault(cv => cv.UserId == user.Id) == null) - { - conversationIsNew = true; - var conversation = new Conversation - { - UserId = user.Id, - ChatId = chat.Id - }; - await _navigatorDbContext.Conversations.AddAsync(conversation, cancellationToken); - } - - if (userIsNew || chatIsNew || conversationIsNew) - { - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - } - - public TUser FindUser(int id) - { - return _navigatorDbContext.Users.Find(id); - } - - public async Task FindUserAsync(int id, CancellationToken cancellationToken = default) - { - return await _navigatorDbContext.Users.FindAsync(id); - } - - public IEnumerable FindAllUsersAsync(CancellationToken cancellationToken = default) - { - return _navigatorDbContext.Users.AsEnumerable(); - } - - public TChat FindChat(long id) - { - return _navigatorDbContext.Chats.Find(id); - } - - public async Task FindChatAsync(long id, CancellationToken cancellationToken = default) - { - return await _navigatorDbContext.Chats.FindAsync(id); - } - - public IEnumerable FindAllChatsAsync(CancellationToken cancellationToken = default) - { - return _navigatorDbContext.Chats.AsEnumerable(); - } - - public async Task MigrateFromGroup(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default) - { - var chat = await FindChatAsync(telegramMessage.Chat.Id, cancellationToken); - - chat.Id = telegramMessage.MigrateToChatId; - chat.Type = Chat.ChatType.Supergroup; - - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - - public async Task MigrateToSupergroup(Telegram.Bot.Types.Message telegramMessage, CancellationToken cancellationToken = default) - { - var chat = await FindChatAsync(telegramMessage.MigrateFromChatId, cancellationToken); - - chat.Id = telegramMessage.Chat.Id; - chat.Type = Chat.ChatType.Supergroup; - - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - - public async Task ChatTitleChanged(Message telegramMessage, CancellationToken cancellationToken = default) - { - var chat = await FindChatAsync(telegramMessage.MigrateFromChatId, cancellationToken); - - chat.Title = telegramMessage.NewChatTitle; - - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - - public async Task ChatMemberLeft(Message telegramMessage, CancellationToken cancellationToken = default) - { - var conversation = await _navigatorDbContext.Conversations - .Where(c => c.ChatId == telegramMessage.Chat.Id && c.UserId == telegramMessage.From.Id) - .SingleOrDefaultAsync(cancellationToken); - - _navigatorDbContext.Remove(conversation); - - await _navigatorDbContext.SaveChangesAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/DefaultUserMapper.cs b/src/Navigator.Extensions.Store/DefaultUserMapper.cs deleted file mode 100644 index 0d6f893..0000000 --- a/src/Navigator.Extensions.Store/DefaultUserMapper.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Store -{ - public class DefaultUserMapper : IUserMapper - { - public User Parse(Telegram.Bot.Types.User user) - { - return new User - { - Id = user.Id, - IsBot = user.IsBot, - LanguageCode = user.LanguageCode, - Username = user.Username - }; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Entities/Chat.cs b/src/Navigator.Extensions.Store/Entities/Chat.cs new file mode 100644 index 0000000..7c1fd57 --- /dev/null +++ b/src/Navigator.Extensions.Store/Entities/Chat.cs @@ -0,0 +1,26 @@ +namespace Navigator.Extensions.Store.Entities; + +public class Chat : Navigator.Entities.Chat +{ + public Chat() + { + Users = new List(); + Conversations = new List(); + Data = new Dictionary(); + FirstInteractionAt = DateTime.UtcNow; + } + + /// + /// Users related to the chat. + /// + public ICollection Users { get; set; } + + public ICollection Conversations { get; set; } + + public IDictionary Data { get; set; } + + /// + /// Date of first interaction for this chat. + /// + public DateTime FirstInteractionAt { get; set; } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Entities/Conversation.cs b/src/Navigator.Extensions.Store/Entities/Conversation.cs new file mode 100644 index 0000000..c7447b8 --- /dev/null +++ b/src/Navigator.Extensions.Store/Entities/Conversation.cs @@ -0,0 +1,28 @@ +namespace Navigator.Extensions.Store.Entities; + +public class Conversation : Navigator.Entities.Conversation +{ + public Conversation() + { + Data = new Dictionary(); + FirstInteractionAt = DateTime.UtcNow; + } + public Conversation(User user, Chat chat) : base(user, chat) + { + Data = new Dictionary(); + FirstInteractionAt = DateTime.UtcNow; + } + + // public new Guid Id { get; set; } + + public new Chat Chat { get; set; } + + public new User User { get; set; } + + public IDictionary Data { get; set; } + + /// + /// Date of first interaction for this chat. + /// + public DateTime FirstInteractionAt { get; set; } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Entities/User.cs b/src/Navigator.Extensions.Store/Entities/User.cs new file mode 100644 index 0000000..d61d58d --- /dev/null +++ b/src/Navigator.Extensions.Store/Entities/User.cs @@ -0,0 +1,26 @@ +namespace Navigator.Extensions.Store.Entities; + +public class User : Navigator.Entities.User +{ + public User() + { + Chats = new List(); + Conversations = new List(); + Data = new Dictionary(); + FirstInteractionAt = DateTime.UtcNow; + } + + /// + /// Chats related to the user. + /// + public IList Chats { get; set; } + + public ICollection Conversations { get; set; } + + public IDictionary Data { get; set; } + + /// + /// Date of first interaction for this chat. + /// + public DateTime FirstInteractionAt { get; set; } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Extractors/IDataExtractor.cs b/src/Navigator.Extensions.Store/Extractors/IDataExtractor.cs new file mode 100644 index 0000000..3b6d346 --- /dev/null +++ b/src/Navigator.Extensions.Store/Extractors/IDataExtractor.cs @@ -0,0 +1,9 @@ +using Navigator.Entities; + +namespace Navigator.Extensions.Store.Extractors; + +public interface IDataExtractor +{ + Dictionary From(Conversation source); + public bool Maps(Type type); +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/INavigatorStore.cs b/src/Navigator.Extensions.Store/INavigatorStore.cs new file mode 100644 index 0000000..f6ddc28 --- /dev/null +++ b/src/Navigator.Extensions.Store/INavigatorStore.cs @@ -0,0 +1,21 @@ +using Navigator.Entities; + +namespace Navigator.Extensions.Store; + +public interface INavigatorStore +{ + public IDictionary? GetAllData(User user); + public IDictionary? GetAllData(Chat chat); + public Task?> GetAllDataAsync(User user, CancellationToken cancellationToken = default); + public Task?> GetAllDataAsync(Chat chat, CancellationToken cancellationToken = default); + + public string? TryGetData(User user, string key); + public string? TryGetData(Chat chat, string key); + public Task TryGetDataAsync(User user, string key, CancellationToken cancellationToken = default); + public Task TryGetDataAsync(Chat chat, string key, CancellationToken cancellationToken = default); + + public bool TryAddData(User user, string key, object data, bool force = false); + public bool TryAddData(Chat chat, string key, object data, bool force = false); + public Task TryAddDataAsync(User user, string key, object data, bool force = false, CancellationToken cancellationToken = default); + public Task TryAddDataAsync(Chat chat, string key, object data, bool force = false, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Navigator.Extensions.Store.csproj b/src/Navigator.Extensions.Store/Navigator.Extensions.Store.csproj index b5e6ecc..ddfeffc 100644 --- a/src/Navigator.Extensions.Store/Navigator.Extensions.Store.csproj +++ b/src/Navigator.Extensions.Store/Navigator.Extensions.Store.csproj @@ -1,42 +1,32 @@ - net5.0 + net6.0 + true enable - true true - 0.9.100-beta - Navigator.Extensions.Store - Navigator Extensions Store + Navigator.Extensions.Store Lucas Maximiliano Marino - Store extension for Navigator Framework. + Persistence extension for Navigator Framework bots, requires provider implementations. https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE https://github.com/navigatorframework/navigator - Telegram, Bot, Framework, Navigator, EF Core, Store, Database, Extension - true - Copyright © Lucas Maximiliano Marino 2021 https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png + Store, Bot, Extension, Navigator + true + AGPL-3.0-only + Copyright © Lucas Maximiliano Marino 2022 - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + - - - - - diff --git a/src/Navigator.Extensions.Store/NavigatorExtensionConfigurationExtensions.cs b/src/Navigator.Extensions.Store/NavigatorExtensionConfigurationExtensions.cs new file mode 100644 index 0000000..fb3adfa --- /dev/null +++ b/src/Navigator.Extensions.Store/NavigatorExtensionConfigurationExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Navigator.Configuration; +using Navigator.Configuration.Extension; +using Navigator.Context.Extensions; +using Navigator.Extensions.Store.Bundled; +using Navigator.Extensions.Store.Context; +using Navigator.Extensions.Store.Context.Extension; + +namespace Navigator.Extensions.Store; + +public static class NavigatorExtensionConfigurationExtensions +{ + public static NavigatorConfiguration Store(this NavigatorExtensionConfiguration extensionConfiguration, Action? dbContextOptions = default) + { + var temporal = new DbContextOptionsBuilder(); + + dbContextOptions?.Invoke(temporal); + + return extensionConfiguration.Extension(configuration => + { + + configuration.Services.AddDbContext(dbContextOptions); + + configuration.Services.AddScoped(); + configuration.Services.AddScoped(); + + configuration.Services.AddScoped(); + + foreach (var extension in temporal.Options.Extensions.OfType()) + { + extension.ExtensionServices?.Invoke(configuration.Services); + } + }); + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/NavigatorOptionsCollectionExtensions.cs b/src/Navigator.Extensions.Store/NavigatorOptionsCollectionExtensions.cs deleted file mode 100644 index 84a4e7d..0000000 --- a/src/Navigator.Extensions.Store/NavigatorOptionsCollectionExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Extensions.Store -{ - public static class NavigatorOptionsCollectionExtensions - { - #region UserMapper - - private const string UserTypeKey = "_navigator.extensions.store.options.user_type"; - - public static void SetUserType(this INavigatorOptions navigatorOptions) where TUser : User - { - navigatorOptions.TryRegisterOption(UserTypeKey, typeof(TUser)); - } - - public static Type? GetUserType(this INavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(UserTypeKey); - } - - #endregion - - #region ChatMapper - - private const string ChatTypeKey = "_navigator.extensions.store.options.chat_type"; - - public static void SetChatType(this INavigatorOptions navigatorOptions) - { - navigatorOptions.TryRegisterOption(ChatTypeKey, typeof(TChat)); - } - - public static Type? GetChatType(this INavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(ChatTypeKey); - } - - #endregion - - #region UserMapper - - private const string UserMapperKey = "_navigator.extensions.store.options.user_mapper"; - - public static void SetUserMapper(this INavigatorOptions navigatorOptions) - { - navigatorOptions.ForceRegisterOption(UserMapperKey, typeof(TUserMapper)); - } - - public static Type? GetUserMapper(this INavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(UserMapperKey); - } - - #endregion - - #region ChatMapper - - private const string ChatMapperKey = "_navigator.extensions.store.options.chat_mapper"; - - public static void SetChatMapper(this INavigatorOptions navigatorOptions) - { - navigatorOptions.ForceRegisterOption(ChatMapperKey, typeof(TChatMapper)); - } - - public static Type? GetChatMapper(this INavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(ChatMapperKey); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/NavigatorServiceCollectionExtensions.cs b/src/Navigator.Extensions.Store/NavigatorServiceCollectionExtensions.cs deleted file mode 100644 index 2ae9024..0000000 --- a/src/Navigator.Extensions.Store/NavigatorServiceCollectionExtensions.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; -using Navigator.Extensions.Store.Context; -using Navigator.Extensions.Store.Provider; - - namespace Navigator.Extensions.Store -{ - public static class NavigatorServiceCollectionExtensions - { - #region Default Implementation - - public static NavigatorBuilder AddNavigatorStore(this NavigatorBuilder navigatorBuilder, Action dbContextOptions = default, - Action options = default) - { - navigatorBuilder.Options.SetUserMapper(); - navigatorBuilder.Options.SetChatMapper(); - - navigatorBuilder.AddNavigatorStore(dbContextOptions, options); - - return navigatorBuilder; - } - - #endregion - - public static NavigatorBuilder AddNavigatorStore(this NavigatorBuilder navigatorBuilder, Action dbContextOptions = default, - Action options = default) - where TContext : NavigatorDbContext - where TUser : User - { - navigatorBuilder.Options.SetChatMapper(); - - return navigatorBuilder.AddNavigatorStore(dbContextOptions, options); - } - - public static NavigatorBuilder AddNavigatorStore(this NavigatorBuilder navigatorBuilder, Action? dbContextOptions = default, - Action? options = default) - where TContext : NavigatorDbContext - where TUser : User - where TChat : Chat - { - navigatorBuilder.Options.SetUserType(); - navigatorBuilder.Options.SetChatType(); - - options?.Invoke(navigatorBuilder.Options); - - navigatorBuilder.Services.AddDbContext(dbContextOptions); - - RegisterUserMapper(navigatorBuilder); - - RegisterChatMapper(navigatorBuilder); - - navigatorBuilder.Services.AddScoped, DefaultEntityManager>(); - navigatorBuilder.Services.TryAddEnumerable(ServiceDescriptor.Scoped>()); - navigatorBuilder.Services.TryAddEnumerable(ServiceDescriptor.Scoped>()); - navigatorBuilder.Services.TryAddEnumerable(ServiceDescriptor.Scoped>()); - - navigatorBuilder.RegisterOrReplaceOptions(); - - return navigatorBuilder; - } - - private static void RegisterUserMapper(NavigatorBuilder navigatorBuilder) where TUser : User - { - if (navigatorBuilder.Options.GetUserMapper() is not null && typeof(IUserMapper).IsAssignableFrom(navigatorBuilder.Options.GetUserMapper())) - { - navigatorBuilder.Services.AddScoped(typeof(IUserMapper), navigatorBuilder.Options.GetUserMapper()!); - } - else - { - throw new ArgumentException("TODO"); - } - } - - private static void RegisterChatMapper(NavigatorBuilder navigatorBuilder) where TChat : Chat - { - if (navigatorBuilder.Options.GetChatMapper() is not null && typeof(IChatMapper).IsAssignableFrom(navigatorBuilder.Options.GetChatMapper())) - { - navigatorBuilder.Services.AddScoped(typeof(IChatMapper), navigatorBuilder.Options.GetChatMapper()!); - } - else - { - throw new ArgumentException("TODO"); - } - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/NavigatorStore.cs b/src/Navigator.Extensions.Store/NavigatorStore.cs new file mode 100644 index 0000000..76ed7a6 --- /dev/null +++ b/src/Navigator.Extensions.Store/NavigatorStore.cs @@ -0,0 +1,240 @@ +using System.Text.Json; +using Navigator.Entities; +using Navigator.Extensions.Store.Context; + +namespace Navigator.Extensions.Store; + +public class NavigatorStore : INavigatorStore +{ + private readonly NavigatorDbContext _dbContext; + + /// + /// Default constructor. + /// + /// + public NavigatorStore(NavigatorDbContext dbContext) + { + _dbContext = dbContext; + } + + /// + public IDictionary? GetAllData(User user) + { + return _dbContext.Users.Find(user.Id)?.Data; + } + + /// + public IDictionary? GetAllData(Chat chat) + { + return _dbContext.Users.Find(chat.Id)?.Data; + } + + /// + public async Task?> GetAllDataAsync(User user, CancellationToken cancellationToken = default) + { + return (await _dbContext.Users.FindAsync(new object?[] { user.Id }, cancellationToken))?.Data; + } + + /// + public async Task?> GetAllDataAsync(Chat chat, CancellationToken cancellationToken = default) + { + return (await _dbContext.Chats.FindAsync(new object?[] { chat?.Id }, cancellationToken))?.Data; + } + + /// + public string? TryGetData(User user, string key) + { + var data = GetAllData(user); + + if (data?.TryGetValue(key, out var value) is not null) + { + return value; + } + + return default; + } + + public string? TryGetData(Chat chat, string key) + { + var data = GetAllData(chat); + + if (data?.TryGetValue(key, out var value) is not null) + { + return value; + } + + return default; + } + + /// + public async Task TryGetDataAsync(User user, string key, CancellationToken cancellationToken = default) + { + var data = await GetAllDataAsync(user, cancellationToken); + + if (data?.TryGetValue(key, out var value) is not null) + { + return value; + } + + return default; + } + + /// + public async Task TryGetDataAsync(Chat chat, string key, CancellationToken cancellationToken = default) + { + var data = await GetAllDataAsync(chat, cancellationToken); + + if (data?.TryGetValue(key, out var value) is not null) + { + return value; + } + + return default; + } + + /// + public bool TryAddData(User user, string key, object data, bool force = false) + { + var storedUser = _dbContext.Users.Find(user.Id); + + if (storedUser is null) return false; + + try + { + switch (force) + { + case true: + if (storedUser.Data.ContainsKey(key)) storedUser.Data.Remove(key); + return TryAddKeyValue(); + case false: + return TryAddKeyValue(); + } + } + catch (Exception) + { + return false; + } + + bool TryAddKeyValue() + { + if (!storedUser.Data.TryAdd(key, JsonSerializer.Serialize(data))) + { + return false; + } + + _dbContext.SaveChanges(); + + return true; + } + } + + /// + public bool TryAddData(Chat chat, string key, object data, bool force = false) + { + var storedChat = _dbContext.Chats.Find(chat.Id); + + if (storedChat is null) return false; + + try + { + switch (force) + { + case true: + if (storedChat.Data.ContainsKey(key)) storedChat.Data.Remove(key); + return TryAddKeyValue(); + case false: + return TryAddKeyValue(); + } + } + catch (Exception) + { + return false; + } + + bool TryAddKeyValue() + { + if (!storedChat.Data.TryAdd(key, JsonSerializer.Serialize(data))) + { + return false; + } + + _dbContext.SaveChanges(); + + return true; + } + } + + /// + public async Task TryAddDataAsync(User user, string key, object data, bool force = false, CancellationToken cancellationToken = default) + { + var storedUser = await _dbContext.Chats.FindAsync(new object?[] { user.Id }, cancellationToken); + + if (storedUser is null) return false; + + try + { + switch (force) + { + case true: + if (storedUser.Data.ContainsKey(key)) storedUser.Data.Remove(key); + return await TryAddKeyValueAsync(); + case false: + return await TryAddKeyValueAsync(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + async Task TryAddKeyValueAsync() + { + if (!storedUser.Data.TryAdd(key, JsonSerializer.Serialize(data))) + { + return false; + } + + await _dbContext.SaveChangesAsync(cancellationToken); + + return true; + } + } + + /// + public async Task TryAddDataAsync(Chat chat, string key, object data, bool force = false, CancellationToken cancellationToken = default) + { + var storedChat = await _dbContext.Chats.FindAsync(new object?[] { chat.Id }, cancellationToken); + + if (storedChat is null) return false; + + try + { + switch (force) + { + case true: + if (storedChat.Data.ContainsKey(key)) storedChat.Data.Remove(key); + return await TryAddKeyValueAsync(); + case false: + return await TryAddKeyValueAsync(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + async Task TryAddKeyValueAsync() + { + if (!storedChat.Data.TryAdd(key, JsonSerializer.Serialize(data))) + { + return false; + } + + await _dbContext.SaveChangesAsync(cancellationToken); + + return true; + } + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/NavigatorStoreException.cs b/src/Navigator.Extensions.Store/NavigatorStoreException.cs new file mode 100644 index 0000000..f180adc --- /dev/null +++ b/src/Navigator.Extensions.Store/NavigatorStoreException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Navigator.Extensions.Store; + +public class NavigatorStoreException : NavigatorException +{ + public NavigatorStoreException() + { + } + + protected NavigatorStoreException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public NavigatorStoreException(string? message) : base(message) + { + } + + public NavigatorStoreException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Provider/DefaultChatContextProvider.cs b/src/Navigator.Extensions.Store/Provider/DefaultChatContextProvider.cs deleted file mode 100644 index d62e73a..0000000 --- a/src/Navigator.Extensions.Store/Provider/DefaultChatContextProvider.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Extensions; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -using Chat = Navigator.Extensions.Store.Abstractions.Entity.Chat; -using User = Navigator.Extensions.Store.Abstractions.Entity.User; - -namespace Navigator.Extensions.Store.Provider -{ - public class DefaultChatContextProvider : INavigatorContextExtensionProvider - where TUser : User - where TChat : Chat - { - public int Order => 500; - - private readonly ILogger> _logger; - private readonly IEntityManager _entityManager; - - public DefaultChatContextProvider(ILogger> logger, IEntityManager entityManager) - { - _logger = logger; - _entityManager = entityManager; - } - - public async Task<(string?, object?)> Process(Update update) - { - _logger.LogDebug("Processing update {UpdateId} of type {UpdateType} from {Provider}", update.Id, update.Type.ToString(), nameof(DefaultChatContextProvider)); - - TChat chat; - - switch (update.Type) - { - case UpdateType.Message: - await _entityManager.Handle(update.Message.From, update.Message.Chat); - chat = await _entityManager.FindChatAsync(update.Message.Chat.Id); - break; - case UpdateType.EditedMessage: - await _entityManager.Handle(update.EditedMessage.From, update.EditedMessage.Chat); - chat = await _entityManager.FindChatAsync(update.EditedMessage.Chat.Id); - break; - case UpdateType.ChannelPost: - await _entityManager.Handle(update.ChannelPost.From, update.ChannelPost.Chat); - chat = await _entityManager.FindChatAsync(update.ChannelPost.Chat.Id); - break; - case UpdateType.EditedChannelPost: - await _entityManager.Handle(update.EditedChannelPost.From, update.EditedChannelPost.Chat); - chat = await _entityManager.FindChatAsync(update.EditedChannelPost.Chat.Id); - break; - default: - return default; - } - - return (INavigatorContextExtensions.DefaultChatKey, chat); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Provider/DefaultUserContextProvider.cs b/src/Navigator.Extensions.Store/Provider/DefaultUserContextProvider.cs deleted file mode 100644 index 3b41d3a..0000000 --- a/src/Navigator.Extensions.Store/Provider/DefaultUserContextProvider.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Extensions; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -using Chat = Navigator.Extensions.Store.Abstractions.Entity.Chat; -using User = Navigator.Extensions.Store.Abstractions.Entity.User; - -namespace Navigator.Extensions.Store.Provider -{ - public class DefaultUserContextProvider : INavigatorContextExtensionProvider - where TUser : User - where TChat : Chat - { - public int Order => 500; - - private readonly ILogger> _logger; - private readonly IEntityManager _entityManager; - - public DefaultUserContextProvider(ILogger> logger, IEntityManager entityManager) - { - _logger = logger; - _entityManager = entityManager; - } - - public async Task<(string?, object?)> Process(Update update) - { - _logger.LogDebug("Processing update {UpdateId} of type {UpdateType} from {Provider}", update.Id, update.Type.ToString(), nameof(DefaultChatContextProvider)); - - TUser user; - - switch (update.Type) - { - case UpdateType.Message: - await _entityManager.Handle(update.Message.From, update.Message.Chat); - user = await _entityManager.FindUserAsync(update.Message.From.Id); - break; - case UpdateType.InlineQuery: - await _entityManager.Handle(update.InlineQuery.From); - user = await _entityManager.FindUserAsync(update.InlineQuery.From.Id); - break; - case UpdateType.ChosenInlineResult: - await _entityManager.Handle(update.ChosenInlineResult.From); - user = await _entityManager.FindUserAsync(update.ChosenInlineResult.From.Id); - break; - case UpdateType.CallbackQuery: - await _entityManager.Handle(update.CallbackQuery.From); - user = await _entityManager.FindUserAsync(update.CallbackQuery.From.Id); - break; - case UpdateType.EditedMessage: - await _entityManager.Handle(update.EditedMessage.From, update.EditedMessage.Chat); - user = await _entityManager.FindUserAsync(update.EditedMessage.From.Id); - break; - case UpdateType.ChannelPost: - await _entityManager.Handle(update.ChannelPost.From, update.ChannelPost.Chat); - user = await _entityManager.FindUserAsync(update.ChannelPost.From.Id); - break; - case UpdateType.EditedChannelPost: - await _entityManager.Handle(update.EditedChannelPost.From, update.EditedChannelPost.Chat); - user = await _entityManager.FindUserAsync(update.EditedChannelPost.From.Id); - break; - case UpdateType.ShippingQuery: - await _entityManager.Handle(update.ShippingQuery.From); - user = await _entityManager.FindUserAsync(update.ShippingQuery.From.Id); - break; - case UpdateType.PreCheckoutQuery: - await _entityManager.Handle(update.PreCheckoutQuery.From); - user = await _entityManager.FindUserAsync(update.PreCheckoutQuery.From.Id); - break; - default: - return default; - } - - return (INavigatorContextExtensions.DefaultUserKey, user); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Extensions.Store/Provider/UpdateDataContextProvider.cs b/src/Navigator.Extensions.Store/Provider/UpdateDataContextProvider.cs deleted file mode 100644 index d00af98..0000000 --- a/src/Navigator.Extensions.Store/Provider/UpdateDataContextProvider.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Navigator.Extensions.Store.Abstractions; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -using Chat = Navigator.Extensions.Store.Abstractions.Entity.Chat; -using User = Navigator.Extensions.Store.Abstractions.Entity.User; - -namespace Navigator.Extensions.Store.Provider -{ - public class UpdateDataContextProvider : INavigatorContextExtensionProvider - where TUser : User - where TChat : Chat - { - public int Order => 250; - - private readonly ILogger> _logger; - private readonly IEntityManager _entityManager; - - public UpdateDataContextProvider(ILogger> logger, IEntityManager entityManager) - { - _logger = logger; - _entityManager = entityManager; - } - - public async Task<(string?, object?)> Process(Update update) - { - if (update.Type == UpdateType.Message || update.Type == UpdateType.ChannelPost) - { - switch (update.Message.Type) - { - case MessageType.ChatMembersAdded: - await AddMembers(update); - break; - case MessageType.ChatMemberLeft: - await _entityManager.ChatMemberLeft(update.Message); - break; - case MessageType.ChatTitleChanged: - await _entityManager.ChatTitleChanged(update.Message); - break; - case MessageType.MigratedToSupergroup: - await _entityManager.MigrateToSupergroup(update.Message); - break; - case MessageType.MigratedFromGroup: - await _entityManager.MigrateFromGroup(update.Message); - break; - } - } - - return default; - } - - private async Task AddMembers(Update update) - { - foreach (var newUser in update.Message.NewChatMembers) - { - await _entityManager.Handle(newUser, update.Message.Chat); - } - } - } -} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/ActionHandlerExtensions.cs b/src/Navigator.Providers.Telegram/ActionHandlerExtensions.cs new file mode 100644 index 0000000..2dcbdf6 --- /dev/null +++ b/src/Navigator.Providers.Telegram/ActionHandlerExtensions.cs @@ -0,0 +1,17 @@ +using Navigator.Actions; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram; + +public static class ActionHandlerExtensions +{ + public static NavigatorTelegramClient GetTelegramClient(this ActionHandler actionHandler) where T : IAction + { + return actionHandler.NavigatorContext.Provider.GetTelegramClient(); + } + + public static Chat GetTelegramChat(this ActionHandler actionHandler) where T : IAction + { + return actionHandler.NavigatorContext.GetTelegramChat(); + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Bundled/CommandAction.cs b/src/Navigator.Providers.Telegram/Actions/Bundled/CommandAction.cs new file mode 100644 index 0000000..f4c0217 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Bundled/CommandAction.cs @@ -0,0 +1,33 @@ +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Providers.Telegram.Actions.Messages; +using Navigator.Providers.Telegram.Entities; +using Navigator.Providers.Telegram.Extensions; + +namespace Navigator.Providers.Telegram.Actions.Bundled; + +/// +/// Command based action. +/// +[ActionType(nameof(CommandAction))] +public abstract class CommandAction : MessageAction +{ + /// + /// Command. + /// + public readonly string Command; + + /// + /// Any arguments passed with the command. If no arguments were passed, it will be null. + /// + public readonly string? Arguments; + + /// + protected CommandAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var botProfile = NavigatorContextAccessor.NavigatorContext.BotProfile; + + Command = Message.ExtractCommand((botProfile as TelegramBot)?.Username) ?? string.Empty; + Arguments = Message.ExtractArguments(); + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Messages/MessageAction.cs b/src/Navigator.Providers.Telegram/Actions/Messages/MessageAction.cs new file mode 100644 index 0000000..2480797 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Messages/MessageAction.cs @@ -0,0 +1,39 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Messages; + +/// +/// A message based action. +/// +[ActionType(nameof(MessageAction))] +public abstract class MessageAction : BaseAction +{ + /// + /// The original Message. + /// + public Message Message { get; protected set; } + + /// + /// Determines if this message is a reply to another message. + /// + public bool IsReply { get; protected set; } + + /// + /// Determines if this message is a forwarded message. + /// + public bool IsForwarded { get; protected set; } + + /// + protected MessageAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = navigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + Message = update.Message!; + IsReply = update.Message!.ReplyToMessage is not null; + IsForwarded = update.Message.ForwardDate is not null; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/UnknownAction.cs b/src/Navigator.Providers.Telegram/Actions/UnknownAction.cs new file mode 100644 index 0000000..23a568b --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/UnknownAction.cs @@ -0,0 +1,14 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions; + +[ActionType(nameof(UnknownAction))] +public abstract class UnknownAction : BaseAction +{ + /// + protected UnknownAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/CallbackQueryAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/CallbackQueryAction.cs new file mode 100644 index 0000000..dc3c3e4 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/CallbackQueryAction.cs @@ -0,0 +1,45 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// A callback query based action. +/// +[ActionType(nameof(CallbackQueryAction))] +public abstract class CallbackQueryAction : BaseAction +{ + /// + /// The original + /// + public CallbackQuery CallbackQuery { get; protected set; } + + /// + /// The message that originated the callback query. Iy may be null if the message is too old. + /// + public Message? OriginalMessage { get; protected set; } + + /// + /// Any data present on the callback query. + /// + public string? Data { get; protected set; } + + /// + /// True if the callback query is from a game. + /// + public bool IsGameQuery { get; protected set; } + + /// + protected CallbackQueryAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + CallbackQuery = update.CallbackQuery!; + OriginalMessage = CallbackQuery.Message; + Data = string.IsNullOrWhiteSpace(CallbackQuery.Data) ? CallbackQuery.Data : default; + IsGameQuery = CallbackQuery.IsGameQuery; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ChannelCreatedAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ChannelCreatedAction.cs new file mode 100644 index 0000000..3a30fbf --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ChannelCreatedAction.cs @@ -0,0 +1,20 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(ChannelCreatedAction))] +public abstract class ChannelCreatedAction : BaseAction +{ + /// + protected ChannelCreatedAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ChannelPostAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ChannelPostAction.cs new file mode 100644 index 0000000..ee6df97 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ChannelPostAction.cs @@ -0,0 +1,27 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(ChannelPostAction))] +public abstract class ChannelPostAction : BaseAction +{ + /// + /// Channel post message. + /// + public Message ChannelPost { get; protected set; } + + /// + protected ChannelPostAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + ChannelPost = update.ChannelPost!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ChatJoinRequestAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ChatJoinRequestAction.cs new file mode 100644 index 0000000..0e3f0ed --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ChatJoinRequestAction.cs @@ -0,0 +1,27 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(ChatJoinRequestAction))] +public abstract class ChatJoinRequestAction : BaseAction +{ + /// + /// Chat join request. + /// + public ChatJoinRequest Request { get; protected set; } + + /// + protected ChatJoinRequestAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + Request = update.ChatJoinRequest!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ChatMemberAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ChatMemberAction.cs new file mode 100644 index 0000000..f8bfb07 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ChatMemberAction.cs @@ -0,0 +1,27 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(ChatMemberAction))] +public abstract class ChatMemberAction : BaseAction +{ + /// + /// Chat member updated. + /// + public ChatMemberUpdated ChatMemberUpdated { get; set; } + + /// + protected ChatMemberAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + ChatMemberUpdated = update.ChatMember!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ChosenInlineResultAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ChosenInlineResultAction.cs new file mode 100644 index 0000000..ccd0076 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ChosenInlineResultAction.cs @@ -0,0 +1,39 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// Inline result based action. +/// +[ActionType(nameof(ChosenInlineResultAction))] +public abstract class ChosenInlineResultAction : BaseAction +{ + /// + /// The original chosen inline result. + /// + public ChosenInlineResult ChosenInlineResult { get; protected set; } + + /// + /// The chosen result id. + /// + public string ResultId { get; protected set; } + + /// + /// The original query. + /// + public string Query { get; protected set; } + + /// + protected ChosenInlineResultAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + ChosenInlineResult = update.ChosenInlineResult; + ResultId = update.ChosenInlineResult.ResultId; + Query = update.ChosenInlineResult.Query; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/DocumentAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/DocumentAction.cs new file mode 100644 index 0000000..e47be23 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/DocumentAction.cs @@ -0,0 +1,18 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(DocumentAction))] +public abstract class DocumentAction : BaseAction +{ + /// + public DocumentAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + //TODO + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/EditedChannelPostAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/EditedChannelPostAction.cs new file mode 100644 index 0000000..25e5ca8 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/EditedChannelPostAction.cs @@ -0,0 +1,21 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(EditedChannelPostAction))] +public abstract class EditedChannelPostAction : BaseAction +{ + /// + protected EditedChannelPostAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + //TODO + } + + + +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/EditedMessageAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/EditedMessageAction.cs new file mode 100644 index 0000000..0346180 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/EditedMessageAction.cs @@ -0,0 +1,33 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(EditedMessageAction))] +public abstract class EditedMessageAction : BaseAction +{ + /// + /// TODO + /// + public Message OriginalMessage { get; protected set; } + + /// + /// TODO + /// + public Message EditedMessage { get; protected set; } + + /// + protected EditedMessageAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + OriginalMessage = update.Message; + EditedMessage = update.EditedMessage; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/InlineQueryAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/InlineQueryAction.cs new file mode 100644 index 0000000..254c78a --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/InlineQueryAction.cs @@ -0,0 +1,39 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// Inline query based action. +/// +[ActionType(nameof(InlineQueryAction))] +public abstract class InlineQueryAction : BaseAction +{ + /// + /// The original + /// + public InlineQuery InlineQuery { get; protected set; } + + /// + /// The query from the user. + /// + public string Query { get; protected set; } + + /// + /// The offset. + /// + public string Offset { get; protected set; } + + /// + protected InlineQueryAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + InlineQuery = update.InlineQuery!; + Query = update.InlineQuery!.Query; + Offset = update.InlineQuery.Offset; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/MyChatMemberAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/MyChatMemberAction.cs new file mode 100644 index 0000000..30b1395 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/MyChatMemberAction.cs @@ -0,0 +1,27 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(MyChatMemberAction))] +public abstract class MyChatMemberAction : BaseAction +{ + /// + /// Chat member updated. + /// + public ChatMemberUpdated ChatMemberUpdated { get; set; } + + /// + protected MyChatMemberAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + ChatMemberUpdated = update.MyChatMember!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/PollAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/PollAction.cs new file mode 100644 index 0000000..2980c5c --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/PollAction.cs @@ -0,0 +1,27 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(PollAction))] +public abstract class PollAction : BaseAction +{ + /// + /// The original Poll. + /// + public Poll Poll { get; protected set; } + + /// + protected PollAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + Poll = update.Poll!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/PollAnswerAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/PollAnswerAction.cs new file mode 100644 index 0000000..affd707 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/PollAnswerAction.cs @@ -0,0 +1,24 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +[ActionType(nameof(PollAnswerAction))] +public abstract class PollAnswerAction : BaseAction +{ + /// + /// The original Poll. + /// + public PollAnswer Answer { get; protected set; } + + /// + protected PollAnswerAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + var update = NavigatorContextAccessor.NavigatorContext.GetOriginalEvent(); + + Answer = update.PollAnswer!; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/PreCheckoutQueryAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/PreCheckoutQueryAction.cs new file mode 100644 index 0000000..aa973e8 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/PreCheckoutQueryAction.cs @@ -0,0 +1,19 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(PreCheckoutQueryAction))] +public abstract class PreCheckoutQueryAction : BaseAction +{ + /// + protected PreCheckoutQueryAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + //TODO + } + +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/ShippingQueryAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/ShippingQueryAction.cs new file mode 100644 index 0000000..a8c6438 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/ShippingQueryAction.cs @@ -0,0 +1,19 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(ShippingQueryAction))] +public abstract class ShippingQueryAction : BaseAction +{ + /// + protected ShippingQueryAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + //TODO + } + +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Actions/Updates/SupergroupCreatedAction.cs b/src/Navigator.Providers.Telegram/Actions/Updates/SupergroupCreatedAction.cs new file mode 100644 index 0000000..d9d4a1e --- /dev/null +++ b/src/Navigator.Providers.Telegram/Actions/Updates/SupergroupCreatedAction.cs @@ -0,0 +1,19 @@ +using Navigator.Actions; +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Providers.Telegram.Actions.Updates; + +/// +/// TODO +/// +[ActionType(nameof(SupergroupCreatedAction))] +public abstract class SupergroupCreatedAction : BaseAction +{ + /// + protected SupergroupCreatedAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + //TODO + } + +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Entities/TelegramBot.cs b/src/Navigator.Providers.Telegram/Entities/TelegramBot.cs new file mode 100644 index 0000000..ac59a62 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Entities/TelegramBot.cs @@ -0,0 +1,59 @@ +using Navigator.Context; +using Navigator.Entities; + +namespace Navigator.Providers.Telegram.Entities; + +public class TelegramBot : Bot +{ + public TelegramBot(long externalIdentifier) : base($"{nameof(TelegramBot)}.{externalIdentifier.ToString()}") + { + ExternalIdentifier = externalIdentifier; + } + + /// + /// Telegram identifier for the bot. + /// + public long ExternalIdentifier { get; init; } + + /// + /// Username of the bot. + /// + public string Username { get; init; } + + /// + /// First name of the bot. + /// + public string FirstName { get; init; } + + /// + /// Last name of the bot. + /// + /// Optional. + /// + /// + public string? LastName { get; init; } + + /// + /// Whether the bot can join groups or not. + /// + /// Optional. Only available on + /// + /// + public bool? CanJoinGroups { get; set; } + + /// + /// Whether the bot can read all group messages or not. + /// + /// Optional. Only available on + /// + /// + public bool? CanReadAllGroupMessages { get; set; } + + /// + /// Whether the bot supports inline queries or not. + /// + /// Optional. Only available on + /// + /// + public bool? SupportsInlineQueries { get; set; } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Entities/TelegramChat.cs b/src/Navigator.Providers.Telegram/Entities/TelegramChat.cs new file mode 100644 index 0000000..c266a88 --- /dev/null +++ b/src/Navigator.Providers.Telegram/Entities/TelegramChat.cs @@ -0,0 +1,35 @@ +using Navigator.Entities; + +namespace Navigator.Providers.Telegram.Entities; + +public class TelegramChat : Chat +{ + public TelegramChat(long externalIdentifier) : base($"{nameof(TelegramChat)}.{externalIdentifier.ToString()}") + { + ExternalIdentifier = externalIdentifier; + } + + /// + /// Telegram identifier for the chat. + /// + public long ExternalIdentifier { get; init; } + + /// + /// Title of the chat, if any. + /// + public string? Title { get; init; } + + /// + /// Type of the chat, can be any of . + /// + public TelegramChatType Type { get; init; } +} + +public enum TelegramChatType +{ + Private, + Group, + Channel, + Supergroup, + Sender +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Entities/TelegramConversation.cs b/src/Navigator.Providers.Telegram/Entities/TelegramConversation.cs new file mode 100644 index 0000000..f37924c --- /dev/null +++ b/src/Navigator.Providers.Telegram/Entities/TelegramConversation.cs @@ -0,0 +1,27 @@ +using Navigator.Entities; + +namespace Navigator.Providers.Telegram.Entities; + +public class TelegramConversation : Conversation +{ + public TelegramConversation() + { + + } + + public TelegramConversation(TelegramUser user, TelegramChat? chat) : base(user, chat) + { + User = user; + Chat = chat; + } + + /// + /// Telegram user. + /// + public new TelegramUser User { get; init; } + + /// + /// Telegram chat. + /// + public new TelegramChat? Chat { get; init; } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Entities/TelegramUser.cs b/src/Navigator.Providers.Telegram/Entities/TelegramUser.cs new file mode 100644 index 0000000..85b774d --- /dev/null +++ b/src/Navigator.Providers.Telegram/Entities/TelegramUser.cs @@ -0,0 +1,45 @@ +using Navigator.Entities; + +namespace Navigator.Providers.Telegram.Entities; + +public class TelegramUser : User +{ + public TelegramUser(long externalIdentifier) : base($"{nameof(TelegramUser)}.{externalIdentifier.ToString()}") + { + ExternalIdentifier = externalIdentifier; + } + + /// + /// Telegram identifier for the user. + /// + public long ExternalIdentifier { get; init; } + + /// + /// Username of the user, if any. + /// + /// Optional. + /// + /// + public string? Username { get; init; } + + /// + /// First name of the user. + /// + public string FirstName { get; init; } + + /// + /// Last name of the user. + /// + /// Optional. + /// + /// + public string? LastName { get; init; } + + /// + /// Language code of the user. + /// + /// Optional. + /// + /// + public string? LanguageCode { get; init; } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Extensions/TelegramUpdateExtensions.cs b/src/Navigator.Providers.Telegram/Extensions/TelegramUpdateExtensions.cs new file mode 100644 index 0000000..9bcdeff --- /dev/null +++ b/src/Navigator.Providers.Telegram/Extensions/TelegramUpdateExtensions.cs @@ -0,0 +1,93 @@ +using Navigator.Providers.Telegram.Entities; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Chat = Telegram.Bot.Types.Chat; +using User = Telegram.Bot.Types.User; + +namespace Navigator.Providers.Telegram.Extensions; + +internal static class TelegramUpdateExtensions +{ + public static string? ExtractCommand(this Message message, string? botName) + { + if (message.Entities?.First().Type != MessageEntityType.BotCommand) return default; + + var command = message.EntityValues?.First(); + + if (command?.Contains('@') == false) return command; + + if (botName is not null && !command?.Contains(botName) == true) return default; + + command = command?[..command.IndexOf('@')]; + + return command; + } + + public static string? ExtractArguments(this Message message) + { + return message.Text is not null && message.Text.Contains(' ') + ? message.Text.Remove(0, message.Text.IndexOf(' ') + 1) + : default; + } + + public static User? GetUserOrDefault(this Update update) + { + return update.Type switch + { + UpdateType.Message => update.Message?.From, + UpdateType.InlineQuery => update.InlineQuery?.From, + UpdateType.ChosenInlineResult => update.ChosenInlineResult?.From, + UpdateType.CallbackQuery => update.CallbackQuery?.From, + UpdateType.EditedMessage => update.EditedMessage?.From, + UpdateType.ChannelPost => update.ChannelPost?.From, + UpdateType.EditedChannelPost => update.EditedChannelPost?.From, + UpdateType.ShippingQuery => update.ShippingQuery?.From, + UpdateType.PreCheckoutQuery => update.PreCheckoutQuery?.From, + _ => default + }; + } + + public static Chat? GetChatOrDefault(this Update update) + { + return update.Type switch + { + UpdateType.CallbackQuery => update.CallbackQuery?.Message?.Chat, + UpdateType.Message => update.Message?.Chat, + UpdateType.EditedMessage => update.EditedMessage?.Chat, + UpdateType.ChannelPost => update.ChannelPost?.Chat, + UpdateType.EditedChannelPost => update.EditedChannelPost?.Chat, + _ => default + }; + } + + public static TelegramConversation GetConversation(this Update update) + { + var rawUser = update.GetUserOrDefault(); + var rawChat = update.GetChatOrDefault(); + + if (rawUser is null) + { + throw new NavigatorException("No conversation could be built, user not found."); + } + + var user = new TelegramUser(rawUser.Id) + { + Username = rawUser.Username, + FirstName = rawUser.FirstName, + LastName = rawUser.LastName, + LanguageCode = rawUser.LanguageCode + }; + + var chat = default(TelegramChat); + + if (rawChat is not null) + { + chat = new TelegramChat(rawChat.Id) + { + Title = rawChat.Title + }; + } + + return new TelegramConversation(user, chat); + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/Hosted/SetTelegramBotWebHookHostedService.cs b/src/Navigator.Providers.Telegram/Hosted/SetTelegramBotWebHookHostedService.cs new file mode 100644 index 0000000..7dc931a --- /dev/null +++ b/src/Navigator.Providers.Telegram/Hosted/SetTelegramBotWebHookHostedService.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Navigator.Configuration; +using Telegram.Bot; + +namespace Navigator.Providers.Telegram.Hosted; + +/// +/// WebHook service for navigator's telegram provider. +/// +public class SetTelegramBotWebHookHostedService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly string _webHookUrl; + + /// + /// Default constructor. + /// + /// + /// + /// + /// + public SetTelegramBotWebHookHostedService(ILogger logger, IServiceScopeFactory serviceScopeFactory, + NavigatorOptions navigatorOptions) + { + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; + + if (string.IsNullOrWhiteSpace(navigatorOptions.GetWebHookBaseUrl())) + { + throw new ArgumentNullException(nameof(navigatorOptions), "An URL for WebHook is required."); + } + + _webHookUrl = $"{navigatorOptions.GetWebHookBaseUrl()}/{navigatorOptions.GetWebHookEndpointOrDefault()}"; + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogTrace("Starting with setup of webhook."); + _logger.LogTrace("Using webHook url {WebHookUrl}", _webHookUrl); + + using var scope = _serviceScopeFactory.CreateScope(); + + var navigatorClient = scope.ServiceProvider.GetRequiredService(); + + await navigatorClient.SetWebhookAsync(_webHookUrl, cancellationToken: stoppingToken); + + var me = await navigatorClient.GetMeAsync(stoppingToken); + + _logger.LogInformation($"Telegram Bot Client is receiving updates for bot: @{me.Username} at the url: {_webHookUrl}"); + } +} \ No newline at end of file diff --git a/src/Navigator.Abstractions/Navigator.Abstractions.csproj b/src/Navigator.Providers.Telegram/Navigator.Providers.Telegram.csproj similarity index 50% rename from src/Navigator.Abstractions/Navigator.Abstractions.csproj rename to src/Navigator.Providers.Telegram/Navigator.Providers.Telegram.csproj index ceb2d9c..84629dd 100644 --- a/src/Navigator.Abstractions/Navigator.Abstractions.csproj +++ b/src/Navigator.Providers.Telegram/Navigator.Providers.Telegram.csproj @@ -1,31 +1,28 @@ - net5.0 + net6.0 + true enable - true true - 0.9.99-beta - Navigator.Abstractions - Navigator Framework Abstractions + Navigator.Providers.Telegram Lucas Maximiliano Marino - A highly opinionated telegram bot framework, mainly based on Telegram.Bot. + Telegram provider for Navigator Framework. Implementation based on Telegram.Bot https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE https://github.com/navigatorframework/navigator + https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png Telegram, Bot, Framework, Navigator true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png + AGPL-3.0-only + Copyright © Lucas Maximiliano Marino 2022 - + - + - - + - + diff --git a/src/Navigator.Providers.Telegram/NavigatorContextExtensions.cs b/src/Navigator.Providers.Telegram/NavigatorContextExtensions.cs new file mode 100644 index 0000000..f1981d9 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorContextExtensions.cs @@ -0,0 +1,258 @@ +using Navigator.Context; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace Navigator.Providers.Telegram; + +/// +/// Useful extensions for Navigator Context. +/// +public static class NavigatorContextExtensions +{ + /// + /// TODO + /// + /// + /// + public static NavigatorTelegramClient GetTelegramClient(this INavigatorContext context) + { + return context.Provider.GetTelegramClient(); + } + + // #region TelegramUser + // + // /// + // /// Get a Telegram user. + // /// + // /// + // /// When user is not found. + // /// + // public static User GetTelegramUser(this INavigatorContext ctx) + // { + // var user = ctx.GetTelegramUserOrDefault(); + // + // return user ?? throw new Exception("User not found in update."); + // } + // + // /// + // /// Get a Telegram user or default if not found. + // /// + // /// + // /// + // public static User? GetTelegramUserOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type switch + // { + // UpdateType.Message => ctx.Update.Message.From, + // UpdateType.InlineQuery => ctx.Update.InlineQuery.From, + // UpdateType.ChosenInlineResult => ctx.Update.ChosenInlineResult.From, + // UpdateType.CallbackQuery => ctx.Update.CallbackQuery.From, + // UpdateType.EditedMessage => ctx.Update.EditedMessage.From, + // UpdateType.ChannelPost => ctx.Update.ChannelPost.From, + // UpdateType.EditedChannelPost => ctx.Update.EditedChannelPost.From, + // UpdateType.ShippingQuery => ctx.Update.ShippingQuery.From, + // UpdateType.PreCheckoutQuery => ctx.Update.PreCheckoutQuery.From, + // _ => default + // }; + // } + // + // #endregion + // + #region Chat + + /// + /// Gets a . + /// + /// + /// When chat is not found. + /// + public static Chat GetTelegramChat(this INavigatorContext ctx) + { + var chat = ctx.GetTelegramChatOrDefault(); + + return chat ?? throw new Exception("Chat was not found."); + } + + /// + /// Get a Telegram chat or default if not found. + /// + /// + /// Telegram Chat + public static Chat? GetTelegramChatOrDefault(this INavigatorContext ctx) + { + return ctx.GetOriginalEventOrDefault()?.Type switch + { + UpdateType.CallbackQuery => ctx.GetOriginalEvent().CallbackQuery?.Message?.Chat, + UpdateType.Message => ctx.GetOriginalEvent().Message?.Chat, + UpdateType.EditedMessage => ctx.GetOriginalEvent().EditedMessage?.Chat, + UpdateType.ChannelPost => ctx.GetOriginalEvent().ChannelPost?.Chat, + UpdateType.EditedChannelPost => ctx.GetOriginalEvent().EditedChannelPost?.Chat, + _ => default + }; + } + + #endregion + // + // #region Message + // + // /// + // /// Get a Telegram message. + // /// + // /// + // /// When message is not found. + // /// + // public static Message GetMessage(this INavigatorContext ctx) + // { + // return ctx.GetMessageOrDefault() ?? throw new Exception("Message not found in update."); + // } + // + // /// + // /// Get a Telegram message or default if not found. + // /// + // /// + // /// + // public static Message? GetMessageOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.Message ? ctx.Update.Message : default; + // } + // + // #endregion + // + // #region InlineQuery + // + // /// + // /// Get a Telegram inline query + // /// + // /// + // /// When inline query is not found. + // /// + // public static InlineQuery GetInlineQuery(this INavigatorContext ctx) + // { + // return ctx.GetInlineQueryOrDefault() ?? throw new Exception("InlineQuery not found in update."); + // } + // + // /// + // /// Get a Telegram inline query or default if not found. + // /// + // /// + // /// + // public static InlineQuery? GetInlineQueryOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.InlineQuery ? ctx.Update.InlineQuery : default; + // } + // + // #endregion + // + // #region ChosenInlineResult + // + // public static ChosenInlineResult GetChosenInlineResult(this INavigatorContext ctx) + // { + // return ctx.GetChosenInlineResultOrDefault() ?? throw new Exception("ChosenInlineResult not found in update."); + // } + // + // public static ChosenInlineResult? GetChosenInlineResultOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.ChosenInlineResult ? ctx.Update.ChosenInlineResult : default; + // } + // + // #endregion + // + // #region CallbackQuery + // + // public static CallbackQuery GetCallbackQuery(this INavigatorContext ctx) + // { + // return ctx.GetCallbackQueryOrDefault() ?? throw new Exception("CallbackQuery not found in update."); + // } + // + // public static CallbackQuery? GetCallbackQueryOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.CallbackQuery ? ctx.Update.CallbackQuery : default; + // } + // + // #endregion + // + // #region EditedMessage + // + // public static Message GetEditedMessage(this INavigatorContext ctx) + // { + // return ctx.GetEditedMessageOrDefault() ?? throw new Exception("EditedMessage not found in update."); + // } + // + // public static Message? GetEditedMessageOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.EditedMessage ? ctx.Update.EditedMessage : default; + // } + // + // #endregion + // + // #region ChannelPost + // + // public static Message GetChannelPost(this INavigatorContext ctx) + // { + // return ctx.GetChannelPostOrDefault() ?? throw new Exception("ChannelPost not found in update."); + // } + // + // public static Message? GetChannelPostOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.ChannelPost ? ctx.Update.ChannelPost : default; + // } + // + // #endregion + // + // #region EditedChannelPost + // + // public static Message GetEditedChannelPost(this INavigatorContext ctx) + // { + // return ctx.GetEditedChannelPostOrDefault() ?? throw new Exception("EditedChannelPost not found in update."); + // } + // + // public static Message? GetEditedChannelPostOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.EditedChannelPost ? ctx.Update.EditedChannelPost : default; + // } + // + // #endregion + // + // #region ShippingQuery + // + // public static ShippingQuery GetShippingQuery(this INavigatorContext ctx) + // { + // return ctx.GetShippingQueryOrDefault() ?? throw new Exception("ShippingQuery not found in update."); + // } + // + // public static ShippingQuery? GetShippingQueryOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.ShippingQuery ? ctx.Update.ShippingQuery : default; + // } + // + // #endregion + // + // #region PreCheckoutQuery + // + // public static PreCheckoutQuery GetPreCheckoutQuery(this INavigatorContext ctx) + // { + // return ctx.GetPreCheckoutQueryOrDefault() ?? throw new Exception("PreCheckoutQuery not found in update."); + // } + // + // public static PreCheckoutQuery? GetPreCheckoutQueryOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.PreCheckoutQuery ? ctx.Update.PreCheckoutQuery : default; + // } + // + // #endregion + // + // #region Poll + // + // public static Poll GetPoll(this INavigatorContext ctx) + // { + // return ctx.GetPollOrDefault() ?? throw new Exception("Poll not found in update."); + // } + // + // public static Poll? GetPollOrDefault(this INavigatorContext ctx) + // { + // return ctx.Update.Type == UpdateType.Poll ? ctx.Update.Poll : default; + // } + // + // #endregion +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorProviderConfigurationExtensions.cs b/src/Navigator.Providers.Telegram/NavigatorProviderConfigurationExtensions.cs new file mode 100644 index 0000000..6279f99 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorProviderConfigurationExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Navigator.Configuration; +using Navigator.Configuration.Provider; +using Navigator.Context; +using Navigator.Providers.Telegram.Hosted; + +namespace Navigator.Providers.Telegram; + +public static class NavigatorProviderConfigurationExtensions +{ + public static NavigatorConfiguration Telegram(this NavigatorProviderConfiguration providerConfiguration, + Action options) + { + var telegramProviderOptions = new NavigatorTelegramProviderOptions(); + options.Invoke(telegramProviderOptions); + + return providerConfiguration.Provider(configuration => + { + configuration.Options.Import(telegramProviderOptions.RetrieveAllOptions()); + + configuration.Services.AddSingleton(); + configuration.Services.AddSingleton(sp => sp.GetRequiredService()); + configuration.Services.AddScoped(); + configuration.Services.AddScoped(); + configuration.Services.AddScoped(); + configuration.Services.AddHostedService(); + }); + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorProviderExtensions.cs b/src/Navigator.Providers.Telegram/NavigatorProviderExtensions.cs new file mode 100644 index 0000000..e9fdbfa --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorProviderExtensions.cs @@ -0,0 +1,32 @@ +namespace Navigator.Providers.Telegram; + +public static class NavigatorProviderExtensions +{ + public static NavigatorTelegramClient GetTelegramClient(this INavigatorProvider provider) + { + var client = provider.GetTelegramClientOrDefault(); + + if (client is not null) + { + return client; + } + + throw new Exception($"Navigator client was not of type {nameof(NavigatorTelegramClient)}") + { + Data = + { + {nameof(Type), provider.GetClient().GetType()} + } + }; + } + + public static NavigatorTelegramClient? GetTelegramClientOrDefault(this INavigatorProvider provider) + { + if (provider.GetClient() is NavigatorTelegramClient navigatorTelegramClient) + { + return navigatorTelegramClient; + } + + return default; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorRouteProviderConfigurationExtensions.cs b/src/Navigator.Providers.Telegram/NavigatorRouteProviderConfigurationExtensions.cs new file mode 100644 index 0000000..6526ea4 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorRouteProviderConfigurationExtensions.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Navigator.Configuration; +using Navigator.Configuration.Provider; +using Newtonsoft.Json; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram; + +public static class NavigatorRouteProviderConfigurationExtensions +{ + public static NavigatorRouteConfiguration Telegram(this NavigatorRouteProviderConfiguration routeProviderConfiguration) + { + return routeProviderConfiguration.Provider(builder => + { + using var scope = builder.ServiceProvider.CreateScope(); + + var options = scope.ServiceProvider.GetRequiredService(); + + builder.MapPost(options.GetWebHookEndpointOrDefault(), ProcessTelegramUpdate); + }); + } + + private static async Task ProcessTelegramUpdate(HttpContext context) + { + context.Response.StatusCode = 200; + + if (context.Request.ContentType != "application/json") + { + return; + } + + var telegramUpdate = await ParseTelegramUpdate(context.Request.Body); + + if (telegramUpdate is not null) + { + var navigatorMiddleware = context.RequestServices.GetRequiredService(); + + await navigatorMiddleware.Process(telegramUpdate); + } + } + + private static async Task ParseTelegramUpdate(Stream stream) + { + try + { + var reader = new StreamReader(stream); + var update = JsonConvert.DeserializeObject(await reader.ReadToEndAsync()); + + return update.Id == default ? default : update; + } + catch + { + return default; + } + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorTelegramClient.cs b/src/Navigator.Providers.Telegram/NavigatorTelegramClient.cs new file mode 100644 index 0000000..2b13de0 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorTelegramClient.cs @@ -0,0 +1,34 @@ +using Navigator.Configuration; +using Navigator.Entities; +using Navigator.Providers.Telegram.Entities; +using Telegram.Bot; + +namespace Navigator.Providers.Telegram; + +public class NavigatorTelegramClient : TelegramBotClient, INavigatorClient +{ + /// + /// Builds a . + /// + /// + public NavigatorTelegramClient(INavigatorOptions options) + : base(options.GetTelegramToken() ?? throw new ArgumentNullException()) + { + } + + /// + public async Task GetProfile(CancellationToken cancellationToken = default) + { + var bot = await this.GetMeAsync(cancellationToken); + + return new TelegramBot(bot.Id) + { + Username = bot.Username!, + FirstName = bot.FirstName, + LastName = bot.LastName, + CanJoinGroups = bot.CanJoinGroups, + CanReadAllGroupMessages = bot.CanReadAllGroupMessages, + SupportsInlineQueries = bot.SupportsInlineQueries + }; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptions.cs b/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptions.cs new file mode 100644 index 0000000..007f5f5 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptions.cs @@ -0,0 +1,8 @@ +using Navigator.Configuration; + +namespace Navigator.Providers.Telegram; + +public class NavigatorTelegramProviderOptions : NavigatorOptions +{ + +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptionsExtensions.cs b/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptionsExtensions.cs new file mode 100644 index 0000000..3bd1f43 --- /dev/null +++ b/src/Navigator.Providers.Telegram/NavigatorTelegramProviderOptionsExtensions.cs @@ -0,0 +1,46 @@ +using Navigator.Configuration; + +namespace Navigator.Providers.Telegram; + +public static class NavigatorTelegramProviderOptionsExtensions +{ + #region TelegramToken + + private const string TelegramTokenKey = "_navigator.options.telegram.telegram_token"; + + public static void SetTelegramToken(this NavigatorTelegramProviderOptions navigatorOptions, string telegramToken) + { + navigatorOptions.TryRegisterOption(TelegramTokenKey, telegramToken); + + } + + public static string? GetTelegramToken(this INavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption(TelegramTokenKey); + } + + #endregion + + #region WebHookEndpoint + + private const string WebHookEndpointKey = "_navigator.options.telegram.webhook_endpoint"; + + public static void SetWebHookEndpoint(this NavigatorTelegramProviderOptions navigatorOptions, string webHookEndpoint) + { + navigatorOptions.TryRegisterOption(WebHookEndpointKey, webHookEndpoint); + } + + public static string GetWebHookEndpointOrDefault(this INavigatorOptions navigatorOptions) + { + var webHookEndpoint = navigatorOptions.RetrieveOption(WebHookEndpointKey); + + if (webHookEndpoint is null) + { + navigatorOptions.TryRegisterOption(WebHookEndpointKey,$"telegram/bot/{Guid.NewGuid()}"); + } + + return navigatorOptions.RetrieveOption(WebHookEndpointKey)!; + } + + #endregion +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/TelegramMiddleware.cs b/src/Navigator.Providers.Telegram/TelegramMiddleware.cs new file mode 100644 index 0000000..eac5b08 --- /dev/null +++ b/src/Navigator.Providers.Telegram/TelegramMiddleware.cs @@ -0,0 +1,97 @@ +using Microsoft.Extensions.Logging; +using Navigator.Actions; +using Navigator.Context; +using Navigator.Providers.Telegram.Actions; +using Navigator.Providers.Telegram.Actions.Bundled; +using Navigator.Providers.Telegram.Actions.Messages; +using Navigator.Providers.Telegram.Actions.Updates; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.Payments; + +namespace Navigator.Providers.Telegram; + +public class TelegramMiddleware +{ + private readonly ILogger _logger; + private readonly INavigatorContextFactory _navigatorContextFactory; + private readonly IActionLauncher _actionLauncher; + + public TelegramMiddleware(ILogger logger, INavigatorContextFactory navigatorContextFactory, IActionLauncher actionLauncher) + { + _logger = logger; + _navigatorContextFactory = navigatorContextFactory; + _actionLauncher = actionLauncher; + } + + public async Task Process(Update update) + { + var actionType = DefineActionType(update); + + if (actionType is null) + { + return; + } + + await _navigatorContextFactory.Supply(builder => + { + builder.SetProvider(); + builder.SetActionType(actionType); + builder.SetOriginalEvent(update); + }); + + await _actionLauncher.Launch(); + } + + private static string? DefineActionType(Update update) + { + return update.Type switch + { + UpdateType.Message when update.Message?.Entities?.First().Type == MessageEntityType.BotCommand => nameof(CommandAction), + UpdateType.Message => update.Message?.Type switch + { + MessageType.Document => nameof(DocumentAction), + // MessageType.Location => ActionType.ChatMembersAdded, + // MessageType.Contact => ActionType.ChatMembersAdded, + // MessageType.Game => ActionType.ChatMembersAdded, + // MessageType.Invoice => ActionType.ChatMembersAdded, + // MessageType.SuccessfulPayment => ActionType.ChatMembersAdded, + // MessageType.WebsiteConnected => ActionType.ChatMembersAdded, + // MessageType.ChatMembersAdded => ActionType.ChatMembersAdded, + // MessageType.ChatMemberLeft => ActionType.ChatMemberLeft, + // MessageType.ChatTitleChanged => ActionType.ChatTitleChanged, + // MessageType.ChatPhotoChanged => ActionType.ChatPhotoChanged, + // MessageType.MessagePinned => ActionType.MessagePinned, + // MessageType.ChatPhotoDeleted => ActionType.ChatPhotoDeleted, + // MessageType.GroupCreated => ActionType.GroupCreated, + MessageType.SupergroupCreated => nameof(SupergroupCreatedAction), + MessageType.ChannelCreated => nameof(ChannelCreatedAction), + // MessageType.MigratedToSupergroup => ActionType.MigratedToSupergroup, + // MessageType.MigratedFromGroup => ActionType.MigratedFromGroup, + // MessageType.Dice => ActionType.MigratedFromGroup, + // MessageType.MessageAutoDeleteTimerChanged => ActionType.MigratedFromGroup, + // MessageType.ProximityAlertTriggered => ActionType.MigratedFromGroup, + // MessageType.VoiceChatScheduled => ActionType.MigratedFromGroup, + // MessageType.VoiceChatStarted => ActionType.MigratedFromGroup, + // MessageType.VoiceChatEnded => ActionType.MigratedFromGroup, + // MessageType.VoiceChatParticipantsInvited => ActionType.MigratedFromGroup, + _ => nameof(MessageAction), + }, + UpdateType.InlineQuery => nameof(InlineQueryAction), + UpdateType.ChosenInlineResult => nameof(ChosenInlineResultAction), + UpdateType.CallbackQuery => nameof(CallbackQueryAction), + UpdateType.EditedMessage => nameof(EditedMessageAction), + UpdateType.ChannelPost => nameof(ChannelPostAction), + UpdateType.EditedChannelPost => nameof(EditedChannelPostAction), + UpdateType.ShippingQuery => nameof(ShippingQueryAction), + UpdateType.PreCheckoutQuery => nameof(PreCheckoutQuery), + UpdateType.Poll => nameof(PollAction), + UpdateType.PollAnswer => nameof(PollAnswerAction), + UpdateType.MyChatMember => nameof(MyChatMemberAction), + UpdateType.ChatMember => nameof(ChatMemberAction), + UpdateType.ChatJoinRequest => nameof(ChatJoinRequestAction), + UpdateType.Unknown => nameof(UnknownAction), + _ => default + }; + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/TelegramNavigatorContextBuilderConversationSource.cs b/src/Navigator.Providers.Telegram/TelegramNavigatorContextBuilderConversationSource.cs new file mode 100644 index 0000000..87613b1 --- /dev/null +++ b/src/Navigator.Providers.Telegram/TelegramNavigatorContextBuilderConversationSource.cs @@ -0,0 +1,14 @@ +using Navigator.Context; +using Navigator.Entities; +using Navigator.Providers.Telegram.Extensions; +using Telegram.Bot.Types; + +namespace Navigator.Providers.Telegram; + +internal class TelegramNavigatorContextBuilderConversationSource : INavigatorContextBuilderConversationSource +{ + public async Task GetConversationAsync(object? originalEvent) + { + return await Task.FromResult((originalEvent as Update ?? throw new InvalidOperationException()).GetConversation()); + } +} \ No newline at end of file diff --git a/src/Navigator.Providers.Telegram/TelegramNavigatorProvider.cs b/src/Navigator.Providers.Telegram/TelegramNavigatorProvider.cs new file mode 100644 index 0000000..2dbc2f3 --- /dev/null +++ b/src/Navigator.Providers.Telegram/TelegramNavigatorProvider.cs @@ -0,0 +1,26 @@ +using Navigator.Providers.Telegram.Entities; + +namespace Navigator.Providers.Telegram; + +internal class TelegramNavigatorProvider : INavigatorProvider +{ + private readonly NavigatorTelegramClient _client; + + public string Name { get; init; } = "navigator.provider.telegram"; + + public TelegramNavigatorProvider(NavigatorTelegramClient client) + { + _client = client; + } + + + public INavigatorClient GetClient() + { + return _client; + } + + public Type GetConversationType() + { + return typeof(TelegramConversation); + } +} \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Actions/EchoAction.cs b/src/Navigator.Samples.Echo/Actions/EchoAction.cs index 7b6456e..5546372 100644 --- a/src/Navigator.Samples.Echo/Actions/EchoAction.cs +++ b/src/Navigator.Samples.Echo/Actions/EchoAction.cs @@ -1,24 +1,19 @@ -using Navigator.Abstractions; -using Navigator.Extensions.Actions; +using Navigator.Context; +using Navigator.Providers.Telegram.Actions.Messages; -namespace Navigator.Samples.Echo.Actions +namespace Navigator.Samples.Echo.Actions; + +public class EchoAction : MessageAction { - public class EchoAction : MessageAction + public readonly string MessageToEcho; + + public EchoAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) { - public string MessageToEcho { get; set; } = string.Empty; - - public override IAction Init(INavigatorContext ctx) - { - if (string.IsNullOrWhiteSpace(ctx.Update.Message.Text)) - { - MessageToEcho = ctx.Update.Message.Text; - } - return this; - } + MessageToEcho = !string.IsNullOrWhiteSpace(Message.Text) ? Message.Text : string.Empty; + } - public override bool CanHandle(INavigatorContext ctx) - { - return !string.IsNullOrWhiteSpace(MessageToEcho); - } + public override bool CanHandleCurrentContext() + { + return !string.IsNullOrWhiteSpace(MessageToEcho); } } \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Actions/EchoActionHandler.cs b/src/Navigator.Samples.Echo/Actions/EchoActionHandler.cs index 0bdd8dc..120329d 100644 --- a/src/Navigator.Samples.Echo/Actions/EchoActionHandler.cs +++ b/src/Navigator.Samples.Echo/Actions/EchoActionHandler.cs @@ -1,27 +1,23 @@ using System.Threading; using System.Threading.Tasks; -using MediatR; -using Navigator.Abstractions; -using Navigator.Abstractions.Extensions; -using Navigator.Extensions.Actions; -using Navigator.Extensions.Store.Abstractions.Extensions; -using Navigator.Samples.Echo.Entity; -using Newtonsoft.Json; +using Navigator.Actions; +using Navigator.Context; +using Navigator.Providers.Telegram; +using Telegram.Bot; -namespace Navigator.Samples.Echo.Actions +namespace Navigator.Samples.Echo.Actions; + +public class EchoActionHandler : ActionHandler { - public class EchoActionHandler : ActionHandler + public EchoActionHandler(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) { - public EchoActionHandler(INavigatorContext ctx) : base(ctx) - { - } + } - public override async Task Handle(EchoAction request, CancellationToken cancellationToken) - { - await Ctx.Client.SendTextMessageAsync(Ctx.GetTelegramChat(), request.MessageToEcho, - cancellationToken: cancellationToken); + public override async Task Handle(EchoAction action, CancellationToken cancellationToken) + { + await this.GetTelegramClient().SendTextMessageAsync(this.GetTelegramChat().Id, + action.MessageToEcho, cancellationToken: cancellationToken); - return Unit.Value; - } + return Success(); } } \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/ConfigurationExtension.cs b/src/Navigator.Samples.Echo/ConfigurationExtension.cs deleted file mode 100644 index f6a4de5..0000000 --- a/src/Navigator.Samples.Echo/ConfigurationExtension.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.IO; -using Microsoft.Extensions.Configuration; -using Serilog; -using Serilog.Exceptions; - -namespace Navigator.Samples.Echo -{ - public static class ConfigurationExtension - { - public static IConfiguration LoadConfiguration() - { - return new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) - .AddEnvironmentVariables() - .Build(); - } - - public static ILogger LoadLogger(IConfiguration configuration) - { - var loggerConf = new LoggerConfiguration() - .Enrich.FromLogContext() - .Enrich.WithExceptionDetails() - .WriteTo.Console(); - - if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") - { - loggerConf.MinimumLevel.Verbose(); - } - else - { - loggerConf.MinimumLevel.Debug(); - } - - return loggerConf.CreateLogger(); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Entity/SampleUser.cs b/src/Navigator.Samples.Echo/Entity/SampleUser.cs deleted file mode 100644 index 2bc476f..0000000 --- a/src/Navigator.Samples.Echo/Entity/SampleUser.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Navigator.Extensions.Store.Abstractions; -using Navigator.Extensions.Store.Abstractions.Entity; - -namespace Navigator.Samples.Echo.Entity -{ - public class SampleUser : User - { - public string Secret { get; set; } - } - - public class SampleUserEntityTypeConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.Property(e => e.Secret) - .HasMaxLength(100); - } - } - - public class SampleUserMapper : IUserMapper - { - public SampleUser Parse(Telegram.Bot.Types.User user) - { - return new SampleUser - { - Id = user.Id, - IsBot = user.IsBot, - LanguageCode = user.LanguageCode, - Username = user.Username, - Secret = "no secret" - }; - } - } -} \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.Designer.cs b/src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.Designer.cs deleted file mode 100644 index ab6b922..0000000 --- a/src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.Designer.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Navigator.Samples.Echo.Persistence; - -namespace Navigator.Samples.Echo.Migrations -{ - [DbContext(typeof(NavigatorSampleDbContext))] - [Migration("20210114093457_InitialMigration")] - partial class InitialMigration - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.1"); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Chats"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Conversation", b => - { - b.Property("ChatId") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.HasKey("ChatId", "UserId"); - - b.HasIndex("UserId"); - - b.ToTable("Conversations"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Discriminator") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("IsBot") - .HasColumnType("INTEGER"); - - b.Property("LanguageCode") - .HasColumnType("TEXT"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("User"); - - b.HasDiscriminator("Discriminator").HasValue("User"); - }); - - modelBuilder.Entity("Navigator.Samples.Echo.Entity.SampleUser", b => - { - b.HasBaseType("Navigator.Extensions.Store.Abstractions.Entity.User"); - - b.Property("Secret") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasDiscriminator().HasValue("SampleUser"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Conversation", b => - { - b.HasOne("Navigator.Extensions.Store.Abstractions.Entity.Chat", "Chat") - .WithMany("Conversations") - .HasForeignKey("ChatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Navigator.Extensions.Store.Abstractions.Entity.User", "User") - .WithMany("Conversations") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chat"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Chat", b => - { - b.Navigation("Conversations"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.User", b => - { - b.Navigation("Conversations"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Navigator.Samples.Echo/Migrations/NavigatorSampleDbContextModelSnapshot.cs b/src/Navigator.Samples.Echo/Migrations/NavigatorSampleDbContextModelSnapshot.cs deleted file mode 100644 index ddb143d..0000000 --- a/src/Navigator.Samples.Echo/Migrations/NavigatorSampleDbContextModelSnapshot.cs +++ /dev/null @@ -1,132 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Navigator.Samples.Echo.Persistence; - -namespace Navigator.Samples.Echo.Migrations -{ - [DbContext(typeof(NavigatorSampleDbContext))] - partial class NavigatorSampleDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.1"); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Chats"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Conversation", b => - { - b.Property("ChatId") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.HasKey("ChatId", "UserId"); - - b.HasIndex("UserId"); - - b.ToTable("Conversations"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Discriminator") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("IsBot") - .HasColumnType("INTEGER"); - - b.Property("LanguageCode") - .HasColumnType("TEXT"); - - b.Property("Username") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("User"); - - b.HasDiscriminator("Discriminator").HasValue("User"); - }); - - modelBuilder.Entity("Navigator.Samples.Echo.Entity.SampleUser", b => - { - b.HasBaseType("Navigator.Extensions.Store.Abstractions.Entity.User"); - - b.Property("Secret") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasDiscriminator().HasValue("SampleUser"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Conversation", b => - { - b.HasOne("Navigator.Extensions.Store.Abstractions.Entity.Chat", "Chat") - .WithMany("Conversations") - .HasForeignKey("ChatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Navigator.Extensions.Store.Abstractions.Entity.User", "User") - .WithMany("Conversations") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chat"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.Chat", b => - { - b.Navigation("Conversations"); - }); - - modelBuilder.Entity("Navigator.Extensions.Store.Abstractions.Entity.User", b => - { - b.Navigation("Conversations"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Navigator.Samples.Echo/Navigator.Samples.Echo.csproj b/src/Navigator.Samples.Echo/Navigator.Samples.Echo.csproj index 1d88af2..0744581 100644 --- a/src/Navigator.Samples.Echo/Navigator.Samples.Echo.csproj +++ b/src/Navigator.Samples.Echo/Navigator.Samples.Echo.csproj @@ -1,25 +1,13 @@ - + - net5.0 + net6.0 enable - 9 - - - - - - + + - - - - - - - diff --git a/src/Navigator.Samples.Echo/Persistence/NavigatorSampleDbContext.cs b/src/Navigator.Samples.Echo/Persistence/NavigatorSampleDbContext.cs deleted file mode 100644 index fb5a2e8..0000000 --- a/src/Navigator.Samples.Echo/Persistence/NavigatorSampleDbContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Navigator.Extensions.Store.Abstractions.Entity; -using Navigator.Extensions.Store.Context; -using Navigator.Samples.Echo.Entity; - -namespace Navigator.Samples.Echo.Persistence -{ - public class NavigatorSampleDbContext : NavigatorDbContext - { - public NavigatorSampleDbContext(DbContextOptions options) : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ApplyConfiguration(new SampleUserEntityTypeConfiguration()); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Program.cs b/src/Navigator.Samples.Echo/Program.cs index fc124d5..36c82cd 100644 --- a/src/Navigator.Samples.Echo/Program.cs +++ b/src/Navigator.Samples.Echo/Program.cs @@ -1,44 +1,36 @@ -using System; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Serilog; +using Navigator; +using Navigator.Configuration; +using Navigator.Providers.Telegram; +using Navigator.Samples.Echo.Actions; -namespace Navigator.Samples.Echo -{ - public class Program +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +// For Extensions.Cooldown +builder.Services.AddDistributedMemoryCache(); + +builder.Services + .AddNavigator(options => { - private static IConfiguration Configuration { get; } = ConfigurationExtension.LoadConfiguration(); - - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.UseSerilog(); - }); - - public static void Main(string[] args) - { - Log.Logger = ConfigurationExtension.LoadLogger(Configuration); - - try - { - var host = CreateHostBuilder(args).Build(); - - Log.Information("Starting WebApi"); - - host.Run(); - } - catch (Exception ex) - { - Log.Fatal(ex, "WebApi terminated unexpectedly"); - } - finally - { - Log.CloseAndFlush(); - } - } - } -} \ No newline at end of file + options.SetWebHookBaseUrl(builder.Configuration["BASE_WEBHOOK_URL"]); + options.RegisterActionsFromAssemblies(typeof(EchoAction).Assembly); + }) + .WithProvider.Telegram(options => { options.SetTelegramToken(builder.Configuration["BOT_TOKEN"]); }); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ +} + +app.UseHttpsRedirection(); + +app.MapNavigator() + .ForProvider.Telegram(); + +app.Run(); \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/Startup.cs b/src/Navigator.Samples.Echo/Startup.cs deleted file mode 100644 index efa9d69..0000000 --- a/src/Navigator.Samples.Echo/Startup.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Navigator.Extensions.Shipyard; -using Navigator.Extensions.Store; -using Navigator.Samples.Echo.Entity; -using Navigator.Samples.Echo.Persistence; - -namespace Navigator.Samples.Echo -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddNewtonsoftJson(); - - services.AddMediatR(typeof(Startup).Assembly); - - services.AddApiVersioning(); - - services.AddNavigator( - options => - { - options.SetTelegramToken(Configuration["BOT_TOKEN"]); - options.SetWebHookBaseUrl(Configuration["BASE_WEBHOOK_URL"]); - options.RegisterActionsFromAssemblies(typeof(Startup).Assembly); - } - ).AddNavigatorStore( - builder => - { - builder.UseSqlite(Configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly("Navigator.Samples.Echo")); - }, - options => { options.SetUserMapper(); } - ).AddShipyard(options => - { - options.SetShipyardApiKey(Configuration["SHIPYARD_API_KEY"]); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - using var serviceScope = app.ApplicationServices.GetService()?.CreateScope(); - serviceScope?.ServiceProvider.GetRequiredService().Database.Migrate(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapNavigator(); - }); - } - } -} \ No newline at end of file diff --git a/src/Navigator.Samples.Echo/appsettings.Development.json b/src/Navigator.Samples.Echo/appsettings.Development.json index 2c63c08..1797133 100644 --- a/src/Navigator.Samples.Echo/appsettings.Development.json +++ b/src/Navigator.Samples.Echo/appsettings.Development.json @@ -1,2 +1,3 @@ { + } diff --git a/src/Navigator.Samples.Echo/appsettings.json b/src/Navigator.Samples.Echo/appsettings.json index 0b50a2f..da75fb0 100644 --- a/src/Navigator.Samples.Echo/appsettings.json +++ b/src/Navigator.Samples.Echo/appsettings.json @@ -1,5 +1,4 @@ { - "ConnectionStrings": { - "DefaultConnection": "DataSource=app.db" - } + "BASE_WEBHOOK_URL": "", + "BOT_TOKEN": "" } diff --git a/src/Navigator.Samples.Store/Actions/EchoAction.cs b/src/Navigator.Samples.Store/Actions/EchoAction.cs new file mode 100644 index 0000000..ef47eeb --- /dev/null +++ b/src/Navigator.Samples.Store/Actions/EchoAction.cs @@ -0,0 +1,19 @@ +using Navigator.Context; +using Navigator.Providers.Telegram.Actions.Messages; + +namespace Navigator.Samples.Store.Actions; + +public class EchoAction : MessageAction +{ + public readonly string MessageToEcho; + + public EchoAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + MessageToEcho = !string.IsNullOrWhiteSpace(Message.Text) ? Message.Text : string.Empty; + } + + public override bool CanHandleCurrentContext() + { + return !string.IsNullOrWhiteSpace(MessageToEcho); + } +} \ No newline at end of file diff --git a/src/Navigator.Samples.Store/Actions/EchoActionHandler.cs b/src/Navigator.Samples.Store/Actions/EchoActionHandler.cs new file mode 100644 index 0000000..fc6287e --- /dev/null +++ b/src/Navigator.Samples.Store/Actions/EchoActionHandler.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Navigator.Actions; +using Navigator.Context; +using Navigator.Extensions.Store.Bundled.Extensions; +using Navigator.Providers.Telegram; +using Telegram.Bot; + +namespace Navigator.Samples.Store.Actions; + +public class EchoActionHandler : ActionHandler +{ + public EchoActionHandler(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + } + + public override async Task Handle(EchoAction action, CancellationToken cancellationToken) + { + await this.GetTelegramClient().SendTextMessageAsync(this.GetTelegramChat().Id, + action.MessageToEcho, cancellationToken: cancellationToken); + + await this.GetTelegramClient().SendTextMessageAsync(this.GetTelegramChat().Id, + JsonSerializer.Serialize(await NavigatorContext.GetStore().GetDataAsync(NavigatorContext.Conversation.User, cancellationToken)), + cancellationToken: cancellationToken); + + return Success(); + } +} \ No newline at end of file diff --git a/src/Navigator.Samples.Store/Actions/InlineAction.cs b/src/Navigator.Samples.Store/Actions/InlineAction.cs new file mode 100644 index 0000000..a7d5239 --- /dev/null +++ b/src/Navigator.Samples.Store/Actions/InlineAction.cs @@ -0,0 +1,16 @@ +using Navigator.Context; +using Navigator.Providers.Telegram.Actions.Updates; + +namespace Navigator.Samples.Store.Actions; + +public class InlineAction : InlineQueryAction +{ + public InlineAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + } + + public override bool CanHandleCurrentContext() + { + return true; + } +} \ No newline at end of file diff --git a/src/Navigator.Samples.Store/Actions/InlineActionHandler.cs b/src/Navigator.Samples.Store/Actions/InlineActionHandler.cs new file mode 100644 index 0000000..9ee3704 --- /dev/null +++ b/src/Navigator.Samples.Store/Actions/InlineActionHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Navigator.Actions; +using Navigator.Context; +using Navigator.Providers.Telegram; +using Telegram.Bot; +using Telegram.Bot.Types.InlineQueryResults; + +namespace Navigator.Samples.Store.Actions; + +public class InlineActionHandler : ActionHandler +{ + public InlineActionHandler(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + } + + public override async Task Handle(InlineAction action, CancellationToken cancellationToken) + { + await NavigatorContext.GetTelegramClient().AnswerInlineQueryAsync(action.InlineQuery.Id, new InlineQueryResult[] + { + new InlineQueryResultArticle(Guid.NewGuid().ToString(), "Test", new InputTextMessageContent("Test Message")) + }, cancellationToken: cancellationToken); + + return Success(); + } +} \ No newline at end of file diff --git a/src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.Designer.cs b/src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.Designer.cs new file mode 100644 index 0000000..2a196f5 --- /dev/null +++ b/src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.Designer.cs @@ -0,0 +1,117 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Navigator.Extensions.Store.Context; + +#nullable disable + +namespace Navigator.Samples.Store.Migrations +{ + [DbContext(typeof(NavigatorDbContext))] + [Migration("20220404102457_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Chat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatId"); + + b.HasIndex("UserId"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Conversation", b => + { + b.HasOne("Navigator.Extensions.Store.Entities.Chat", "Chat") + .WithMany("Conversations") + .HasForeignKey("ChatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Navigator.Extensions.Store.Entities.User", "User") + .WithMany("Conversations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chat"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Chat", b => + { + b.Navigation("Conversations"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.User", b => + { + b.Navigation("Conversations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.cs b/src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.cs similarity index 52% rename from src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.cs rename to src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.cs index d7f16a7..bdbd246 100644 --- a/src/Navigator.Samples.Echo/Migrations/20210114093457_InitialMigration.cs +++ b/src/Navigator.Samples.Store/Migrations/20220404102457_InitialMigration.cs @@ -1,7 +1,9 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace Navigator.Samples.Echo.Migrations +#nullable disable + +namespace Navigator.Samples.Store.Migrations { public partial class InitialMigration : Migration { @@ -11,12 +13,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Chats", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Username = table.Column(type: "TEXT", nullable: true), - Title = table.Column(type: "TEXT", nullable: true), - Type = table.Column(type: "INTEGER", nullable: false), - CreatedAt = table.Column(type: "TEXT", nullable: false) + Id = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false), + FirstInteractionAt = table.Column(type: "TEXT", nullable: false) }, constraints: table => { @@ -24,34 +23,31 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateTable( - name: "User", + name: "Users", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - IsBot = table.Column(type: "INTEGER", nullable: false), - LanguageCode = table.Column(type: "TEXT", nullable: true), - Username = table.Column(type: "TEXT", nullable: true), - CreatedAt = table.Column(type: "TEXT", nullable: false), - Discriminator = table.Column(type: "TEXT", nullable: false), - Secret = table.Column(type: "TEXT", maxLength: 100, nullable: true) + Id = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false), + FirstInteractionAt = table.Column(type: "TEXT", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_User", x => x.Id); + table.PrimaryKey("PK_Users", x => x.Id); }); migrationBuilder.CreateTable( name: "Conversations", columns: table => new { - ChatId = table.Column(type: "INTEGER", nullable: false), - UserId = table.Column(type: "INTEGER", nullable: false), - CreatedAt = table.Column(type: "TEXT", nullable: false) + Id = table.Column(type: "TEXT", nullable: false), + ChatId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false), + FirstInteractionAt = table.Column(type: "TEXT", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_Conversations", x => new { x.ChatId, x.UserId }); + table.PrimaryKey("PK_Conversations", x => x.Id); table.ForeignKey( name: "FK_Conversations_Chats_ChatId", column: x => x.ChatId, @@ -59,13 +55,18 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_Conversations_User_UserId", + name: "FK_Conversations_Users_UserId", column: x => x.UserId, - principalTable: "User", + principalTable: "Users", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateIndex( + name: "IX_Conversations_ChatId", + table: "Conversations", + column: "ChatId"); + migrationBuilder.CreateIndex( name: "IX_Conversations_UserId", table: "Conversations", @@ -81,7 +82,7 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "Chats"); migrationBuilder.DropTable( - name: "User"); + name: "Users"); } } } diff --git a/src/Navigator.Samples.Store/Migrations/NavigatorDbContextModelSnapshot.cs b/src/Navigator.Samples.Store/Migrations/NavigatorDbContextModelSnapshot.cs new file mode 100644 index 0000000..5240e5d --- /dev/null +++ b/src/Navigator.Samples.Store/Migrations/NavigatorDbContextModelSnapshot.cs @@ -0,0 +1,115 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Navigator.Extensions.Store.Context; + +#nullable disable + +namespace Navigator.Samples.Store.Migrations +{ + [DbContext(typeof(NavigatorDbContext))] + partial class NavigatorDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Chat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Chats"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChatId") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatId"); + + b.HasIndex("UserId"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstInteractionAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Conversation", b => + { + b.HasOne("Navigator.Extensions.Store.Entities.Chat", "Chat") + .WithMany("Conversations") + .HasForeignKey("ChatId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Navigator.Extensions.Store.Entities.User", "User") + .WithMany("Conversations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chat"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.Chat", b => + { + b.Navigation("Conversations"); + }); + + modelBuilder.Entity("Navigator.Extensions.Store.Entities.User", b => + { + b.Navigation("Conversations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Navigator.Samples.Store/Navigator.Samples.Store.csproj b/src/Navigator.Samples.Store/Navigator.Samples.Store.csproj new file mode 100644 index 0000000..579f5d0 --- /dev/null +++ b/src/Navigator.Samples.Store/Navigator.Samples.Store.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/Navigator.Samples.Store/Program.cs b/src/Navigator.Samples.Store/Program.cs new file mode 100644 index 0000000..855083c --- /dev/null +++ b/src/Navigator.Samples.Store/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Navigator; +using Navigator.Configuration; +using Navigator.Extensions.Store; +using Navigator.Extensions.Store.Context; +using Navigator.Extensions.Store.Telegram; +using Navigator.Providers.Telegram; +using Navigator.Samples.Store.Actions; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +// For Extensions.Cooldown +builder.Services.AddDistributedMemoryCache(); + +builder.Services + .AddNavigator(options => + { + options.SetWebHookBaseUrl(builder.Configuration["BASE_WEBHOOK_URL"]); + options.RegisterActionsFromAssemblies(typeof(EchoAction).Assembly); + }) + .WithProvider.Telegram(options => { options.SetTelegramToken(builder.Configuration["BOT_TOKEN"]); }) + .WithExtension.Store(dbBuilder => + { + dbBuilder.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"), + b => b.MigrationsAssembly("Navigator.Samples.Store")); + }).WithExtension.StoreForTelegram(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ +} + + +using var serviceScope = app.Services.GetService()?.CreateScope(); +serviceScope?.ServiceProvider.GetRequiredService().Database.Migrate(); + +app.UseHttpsRedirection(); + +app.MapNavigator() + .ForProvider.Telegram(); + +app.Run(); \ No newline at end of file diff --git a/src/Navigator.Samples.Store/app.db-shm b/src/Navigator.Samples.Store/app.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/src/Navigator.Samples.Store/app.db-shm differ diff --git a/src/Navigator.Samples.Store/app.db-wal b/src/Navigator.Samples.Store/app.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/src/Navigator.Samples.Store/appsettings.Development.json b/src/Navigator.Samples.Store/appsettings.Development.json new file mode 100644 index 0000000..077404a --- /dev/null +++ b/src/Navigator.Samples.Store/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/src/Navigator.Samples.Store/appsettings.json b/src/Navigator.Samples.Store/appsettings.json new file mode 100644 index 0000000..3fca8c5 --- /dev/null +++ b/src/Navigator.Samples.Store/appsettings.json @@ -0,0 +1,7 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "DataSource=app.db" + }, + "BASE_WEBHOOK_URL": "", + "BOT_TOKEN": "" +} diff --git a/src/Navigator.sln b/src/Navigator.sln index 76befef..1032ab6 100644 --- a/src/Navigator.sln +++ b/src/Navigator.sln @@ -1,38 +1,23 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator", "Navigator\Navigator.csproj", "{6DBAD041-6623-4903-9939-254FDC4EBD60}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Samples.Echo", "Navigator.Samples.Echo\Navigator.Samples.Echo.csproj", "{7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Store", "Navigator.Extensions.Store\Navigator.Extensions.Store.csproj", "{9DEA166A-AE92-438F-8F3D-7C2F777ABBDD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Actions", "Navigator.Extensions.Actions\Navigator.Extensions.Actions.csproj", "{2711B76D-B24C-4A3A-A33F-5A0077C49CB6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{6D6A8E40-8C40-49AA-BEAD-B046D43F511A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{F178DCF2-AD03-44C8-AF8F-95C60664DD10}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{288F4B0C-9EF0-443B-B893-5FC34F9D032B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{895808DE-1CA6-406C-924A-7E43F293CF7D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Abstractions", "Navigator.Abstractions\Navigator.Abstractions.csproj", "{FAB1A6C8-EBEB-4338-B454-75DA358E7DE2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Navigator", "Navigator", "{E6F24E3B-A066-4572-94F0-14665E19EFC7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{E6F24E3B-A066-4572-94F0-14665E19EFC7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Abstractions.Extensions", "Navigator.Abstractions.Extensions\Navigator.Abstractions.Extensions.csproj", "{FACFECB8-1215-4F04-B9B2-F7FB7656804F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{DAA22477-BB29-475C-AD2C-905F81B4AD8E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Store", "Store", "{F26BD166-0ED4-4096-8841-47959645B3DE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Providers.Telegram", "Navigator.Providers.Telegram\Navigator.Providers.Telegram.csproj", "{8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Store.Abstractions", "Navigator.Extensions.Store.Abstractions\Navigator.Extensions.Store.Abstractions.csproj", "{8E680AE7-33EC-4049-A9DC-0F42C6A03184}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Samples.Echo", "Navigator.Samples.Echo\Navigator.Samples.Echo.csproj", "{4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Store.Abstractions.Extensions", "Navigator.Extensions.Store.Abstractions.Extensions\Navigator.Extensions.Store.Abstractions.Extensions.csproj", "{DB8B0A37-3CE6-4B84-8796-819D04D6C70E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{A6DF6F5F-F784-4EF1-97D1-C3DDD2E66B5E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shipyard", "Shipyard", "{E2FF61C3-F081-40C8-9CC3-BBE1F81543F6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Store", "Navigator.Extensions.Store\Navigator.Extensions.Store.csproj", "{E7D6F33B-C30E-4ED2-878C-B2F899CF1805}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Shipyard", "Navigator.Extensions.Shipyard\Navigator.Extensions.Shipyard.csproj", "{90E6A9A5-BD73-4C9D-97A3-FA8021D16921}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Samples.Store", "Navigator.Samples.Store\Navigator.Samples.Store.csproj", "{50766B99-2587-4298-B43C-58DE387F5860}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Shipyard.Abstractions", "Navigator.Extensions.Shipyard.Abstractions\Navigator.Extensions.Shipyard.Abstractions.csproj", "{294C1716-C054-4585-B66E-C79D155AE684}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Navigator.Extensions.Store.Telegram", "Navigator.Extensions.Store.Telegram\Navigator.Extensions.Store.Telegram.csproj", "{342AEA62-E0C8-4B02-A048-5FACD34E9B29}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -44,58 +29,35 @@ Global {6DBAD041-6623-4903-9939-254FDC4EBD60}.Debug|Any CPU.Build.0 = Debug|Any CPU {6DBAD041-6623-4903-9939-254FDC4EBD60}.Release|Any CPU.ActiveCfg = Release|Any CPU {6DBAD041-6623-4903-9939-254FDC4EBD60}.Release|Any CPU.Build.0 = Release|Any CPU - {7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E}.Release|Any CPU.Build.0 = Release|Any CPU - {9DEA166A-AE92-438F-8F3D-7C2F777ABBDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9DEA166A-AE92-438F-8F3D-7C2F777ABBDD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9DEA166A-AE92-438F-8F3D-7C2F777ABBDD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9DEA166A-AE92-438F-8F3D-7C2F777ABBDD}.Release|Any CPU.Build.0 = Release|Any CPU - {2711B76D-B24C-4A3A-A33F-5A0077C49CB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2711B76D-B24C-4A3A-A33F-5A0077C49CB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2711B76D-B24C-4A3A-A33F-5A0077C49CB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2711B76D-B24C-4A3A-A33F-5A0077C49CB6}.Release|Any CPU.Build.0 = Release|Any CPU - {FAB1A6C8-EBEB-4338-B454-75DA358E7DE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAB1A6C8-EBEB-4338-B454-75DA358E7DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAB1A6C8-EBEB-4338-B454-75DA358E7DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAB1A6C8-EBEB-4338-B454-75DA358E7DE2}.Release|Any CPU.Build.0 = Release|Any CPU - {FACFECB8-1215-4F04-B9B2-F7FB7656804F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FACFECB8-1215-4F04-B9B2-F7FB7656804F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FACFECB8-1215-4F04-B9B2-F7FB7656804F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FACFECB8-1215-4F04-B9B2-F7FB7656804F}.Release|Any CPU.Build.0 = Release|Any CPU - {8E680AE7-33EC-4049-A9DC-0F42C6A03184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E680AE7-33EC-4049-A9DC-0F42C6A03184}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E680AE7-33EC-4049-A9DC-0F42C6A03184}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E680AE7-33EC-4049-A9DC-0F42C6A03184}.Release|Any CPU.Build.0 = Release|Any CPU - {DB8B0A37-3CE6-4B84-8796-819D04D6C70E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB8B0A37-3CE6-4B84-8796-819D04D6C70E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB8B0A37-3CE6-4B84-8796-819D04D6C70E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB8B0A37-3CE6-4B84-8796-819D04D6C70E}.Release|Any CPU.Build.0 = Release|Any CPU - {90E6A9A5-BD73-4C9D-97A3-FA8021D16921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90E6A9A5-BD73-4C9D-97A3-FA8021D16921}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90E6A9A5-BD73-4C9D-97A3-FA8021D16921}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90E6A9A5-BD73-4C9D-97A3-FA8021D16921}.Release|Any CPU.Build.0 = Release|Any CPU - {294C1716-C054-4585-B66E-C79D155AE684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {294C1716-C054-4585-B66E-C79D155AE684}.Debug|Any CPU.Build.0 = Debug|Any CPU - {294C1716-C054-4585-B66E-C79D155AE684}.Release|Any CPU.ActiveCfg = Release|Any CPU - {294C1716-C054-4585-B66E-C79D155AE684}.Release|Any CPU.Build.0 = Release|Any CPU + {8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573}.Release|Any CPU.Build.0 = Release|Any CPU + {4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975}.Release|Any CPU.Build.0 = Release|Any CPU + {E7D6F33B-C30E-4ED2-878C-B2F899CF1805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7D6F33B-C30E-4ED2-878C-B2F899CF1805}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7D6F33B-C30E-4ED2-878C-B2F899CF1805}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7D6F33B-C30E-4ED2-878C-B2F899CF1805}.Release|Any CPU.Build.0 = Release|Any CPU + {50766B99-2587-4298-B43C-58DE387F5860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50766B99-2587-4298-B43C-58DE387F5860}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50766B99-2587-4298-B43C-58DE387F5860}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50766B99-2587-4298-B43C-58DE387F5860}.Release|Any CPU.Build.0 = Release|Any CPU + {342AEA62-E0C8-4B02-A048-5FACD34E9B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {342AEA62-E0C8-4B02-A048-5FACD34E9B29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {342AEA62-E0C8-4B02-A048-5FACD34E9B29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {342AEA62-E0C8-4B02-A048-5FACD34E9B29}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {7E8F867E-70E1-4C5B-AE65-A2C6822BFD6E} = {6D6A8E40-8C40-49AA-BEAD-B046D43F511A} - {F178DCF2-AD03-44C8-AF8F-95C60664DD10} = {895808DE-1CA6-406C-924A-7E43F293CF7D} - {E6F24E3B-A066-4572-94F0-14665E19EFC7} = {895808DE-1CA6-406C-924A-7E43F293CF7D} {6DBAD041-6623-4903-9939-254FDC4EBD60} = {E6F24E3B-A066-4572-94F0-14665E19EFC7} - {FAB1A6C8-EBEB-4338-B454-75DA358E7DE2} = {E6F24E3B-A066-4572-94F0-14665E19EFC7} - {2711B76D-B24C-4A3A-A33F-5A0077C49CB6} = {288F4B0C-9EF0-443B-B893-5FC34F9D032B} - {288F4B0C-9EF0-443B-B893-5FC34F9D032B} = {F178DCF2-AD03-44C8-AF8F-95C60664DD10} - {FACFECB8-1215-4F04-B9B2-F7FB7656804F} = {E6F24E3B-A066-4572-94F0-14665E19EFC7} - {F26BD166-0ED4-4096-8841-47959645B3DE} = {F178DCF2-AD03-44C8-AF8F-95C60664DD10} - {9DEA166A-AE92-438F-8F3D-7C2F777ABBDD} = {F26BD166-0ED4-4096-8841-47959645B3DE} - {8E680AE7-33EC-4049-A9DC-0F42C6A03184} = {F26BD166-0ED4-4096-8841-47959645B3DE} - {DB8B0A37-3CE6-4B84-8796-819D04D6C70E} = {F26BD166-0ED4-4096-8841-47959645B3DE} - {E2FF61C3-F081-40C8-9CC3-BBE1F81543F6} = {F178DCF2-AD03-44C8-AF8F-95C60664DD10} - {90E6A9A5-BD73-4C9D-97A3-FA8021D16921} = {E2FF61C3-F081-40C8-9CC3-BBE1F81543F6} - {294C1716-C054-4585-B66E-C79D155AE684} = {E2FF61C3-F081-40C8-9CC3-BBE1F81543F6} + {8E0F6DCA-FAA4-4FA8-B9E0-75E9DC0F3573} = {DAA22477-BB29-475C-AD2C-905F81B4AD8E} + {4F4CEC5E-5FAF-4914-8EC0-E6BC9EB1B975} = {6D6A8E40-8C40-49AA-BEAD-B046D43F511A} + {DAA22477-BB29-475C-AD2C-905F81B4AD8E} = {E6F24E3B-A066-4572-94F0-14665E19EFC7} + {A6DF6F5F-F784-4EF1-97D1-C3DDD2E66B5E} = {E6F24E3B-A066-4572-94F0-14665E19EFC7} + {E7D6F33B-C30E-4ED2-878C-B2F899CF1805} = {A6DF6F5F-F784-4EF1-97D1-C3DDD2E66B5E} + {50766B99-2587-4298-B43C-58DE387F5860} = {6D6A8E40-8C40-49AA-BEAD-B046D43F511A} + {342AEA62-E0C8-4B02-A048-5FACD34E9B29} = {A6DF6F5F-F784-4EF1-97D1-C3DDD2E66B5E} EndGlobalSection EndGlobal diff --git a/src/Navigator/ActionLauncher.cs b/src/Navigator/ActionLauncher.cs deleted file mode 100644 index afa99f3..0000000 --- a/src/Navigator/ActionLauncher.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Navigator.Abstractions; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; - -namespace Navigator -{ - public class ActionLauncher : IActionLauncher - { - protected readonly ILogger Logger; - protected readonly IMediator Mediator; - protected readonly bool MultipleActionsUsage; - protected readonly IEnumerable Actions; - protected readonly INavigatorContext Ctx; - - public ActionLauncher(ILogger logger, NavigatorOptions options, IMediator mediator, IEnumerable actions, INavigatorContext navigatorContext) - { - Logger = logger; - MultipleActionsUsage = options.MultipleActionsUsageIsEnabled(); - Mediator = mediator; - Actions = actions; - Ctx = navigatorContext; - } - - public async Task Launch() - { - Logger.LogTrace("Launching of multiple actions is {MultipleActionLaunchState}.", MultipleActionsUsage ? "active" : "inactive"); - Logger.LogTrace("Starting with action launching."); - - var actions = GetActions(Ctx.Update).ToList(); - - Logger.LogTrace("Found a total of {NumberOfActionsFound} actions to launch.", actions.Count); - - foreach (var action in actions) - { - try - { - Logger.LogTrace("Launching action {ActionName}", action.GetType().Name); - - await Mediator.Send(action); - - Logger.LogTrace("Action {ActionName} successfully launched", action.GetType().Name); - } - catch (Exception e) - { - Logger.LogError(e, "Action {ActionName} finished launch with errors", action.GetType().Name); - } - } - } - - public IEnumerable GetActions(Update update) - { - var actions = new List(); - var actionType = GetActionType(update); - - if (string.IsNullOrWhiteSpace(actionType)) - { - return actions; - } - - if (MultipleActionsUsage) - { - actions = Actions - .Where(a => a.Type == actionType) - .Where(a => a.Init(Ctx).CanHandle(Ctx)) - .OrderBy(a => a.Order) - .ToList(); - } - else - { - var action = Actions - .Where(a => a.Type == actionType) - .OrderBy(a => a.Order) - .FirstOrDefault(a => a.Init(Ctx).CanHandle(Ctx)); - - if (action != null) - { - actions.Add(action); - } - } - - return actions; - } - - public string? GetActionType(Update update) - { - return update.Type switch - { - UpdateType.Message when update.Message.Entities?.First()?.Type == MessageEntityType.BotCommand => ActionType.Command, - UpdateType.Message => update.Message.Type switch - { - MessageType.ChatMembersAdded => ActionType.ChatMembersAdded, - MessageType.ChatMemberLeft => ActionType.ChatMemberLeft, - MessageType.ChatTitleChanged => ActionType.ChatTitleChanged, - MessageType.ChatPhotoChanged => ActionType.ChatPhotoChanged, - MessageType.MessagePinned => ActionType.MessagePinned, - MessageType.ChatPhotoDeleted => ActionType.ChatPhotoDeleted, - MessageType.GroupCreated => ActionType.GroupCreated, - MessageType.SupergroupCreated => ActionType.SupergroupCreated, - MessageType.ChannelCreated => ActionType.ChannelCreated, - MessageType.MigratedToSupergroup => ActionType.MigratedToSupergroup, - MessageType.MigratedFromGroup => ActionType.MigratedFromGroup, - _ => ActionType.Message - }, - UpdateType.InlineQuery => ActionType.InlineQuery, - UpdateType.ChosenInlineResult => ActionType.InlineResultChosen, - UpdateType.CallbackQuery => ActionType.CallbackQuery, - UpdateType.EditedMessage => ActionType.EditedMessage, - UpdateType.ChannelPost => ActionType.ChannelPost, - UpdateType.EditedChannelPost => ActionType.EditedChannelPost, - UpdateType.ShippingQuery => ActionType.ShippingQuery, - UpdateType.PreCheckoutQuery => ActionType.PreCheckoutQuery, - UpdateType.Poll => ActionType.Poll, - UpdateType.Unknown => ActionType.Unknown, - _ => default - }; - } - } -} \ No newline at end of file diff --git a/src/Navigator/Actions/ActionHandler.cs b/src/Navigator/Actions/ActionHandler.cs new file mode 100644 index 0000000..d53a0ef --- /dev/null +++ b/src/Navigator/Actions/ActionHandler.cs @@ -0,0 +1,26 @@ +using MediatR; +using Navigator.Context; + +namespace Navigator.Actions; + +public abstract class ActionHandler : IRequestHandler where TAction : IAction +{ + public INavigatorContext NavigatorContext; + + protected ActionHandler(INavigatorContextAccessor navigatorContextAccessor) + { + NavigatorContext = navigatorContextAccessor.NavigatorContext; + } + + public abstract Task Handle(TAction action, CancellationToken cancellationToken); + + public static Status Success() + { + return new(true); + } + + public static Status Error() + { + return new(false); + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/ActionHandlerExtensions.cs b/src/Navigator/Actions/ActionHandlerExtensions.cs new file mode 100644 index 0000000..7e4403c --- /dev/null +++ b/src/Navigator/Actions/ActionHandlerExtensions.cs @@ -0,0 +1,6 @@ +namespace Navigator.Actions; + +public class ActionHandlerExtensions +{ + +} \ No newline at end of file diff --git a/src/Navigator/Actions/ActionLauncher.cs b/src/Navigator/Actions/ActionLauncher.cs new file mode 100644 index 0000000..4d74114 --- /dev/null +++ b/src/Navigator/Actions/ActionLauncher.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Navigator.Configuration; +using Navigator.Context; + +namespace Navigator.Actions; + +internal class ActionLauncher : IActionLauncher +{ + private readonly ILogger _logger; + private readonly INavigatorContextAccessor _navigatorContextAccessor; + private readonly IServiceProvider _serviceProvider; + private readonly ISender _sender; + private readonly NavigatorOptions _navigatorOptions; + private readonly ImmutableDictionary _actions; + private readonly ImmutableDictionary _priorities; + + public ActionLauncher(ILogger logger, NavigatorOptions navigatorOptions, INavigatorContextAccessor navigatorContextAccessor, IServiceProvider serviceProvider, ISender sender) + { + _logger = logger; + _navigatorContextAccessor = navigatorContextAccessor; + _serviceProvider = serviceProvider; + _sender = sender; + _navigatorOptions = navigatorOptions; + _actions = _navigatorOptions.RetrieveActions(); + _priorities = _navigatorOptions.RetrievePriorities(); + } + + public async Task Launch() + { + var actions = GetActions(); + + foreach (var action in actions) + { + try + { + await _sender.Send(action); + } + catch (Exception e) + { + _logger.LogError(e, "Unhandled exception running {ActionName} ({@ActionData})", action.GetType().Name, action); + } + } + } + + private IEnumerable GetActions() + { + if (string.IsNullOrWhiteSpace(_navigatorContextAccessor.NavigatorContext.ActionType)) + { + return Array.Empty(); + } + + var actions = _actions + .Where(a => + a.Key == _navigatorContextAccessor.NavigatorContext.ActionType || + a.Key == nameof(ProviderAgnosticAction)) + .ToImmutableList(); + + if (_navigatorOptions.MultipleActionsUsageIsEnabled()) + { + return actions + .SelectMany(groups => groups.Value) + .SelectMany(actionType => _serviceProvider.GetServices(actionType) + .Select(action =>((IAction) action!, actionType.FullName))) + .Where(a => a.Item1.CanHandleCurrentContext()) + .OrderBy(a => _priorities.GetValueOrDefault(a.FullName ?? string.Empty, Priority.Default)) + .Select(a => a.Item1) + .AsEnumerable(); + } + + var action = actions + .SelectMany(groups => groups.Value) + .SelectMany(actionType => _serviceProvider.GetServices(actionType) + .Select(action =>((IAction) action!, actionType.FullName))) + .OrderBy(a => _priorities.GetValueOrDefault(a.FullName ?? string.Empty, Priority.Default)) + .Select(a => a.Item1) + .FirstOrDefault(a => a.CanHandleCurrentContext()); + + return action is not null ? new[] {action} : Array.Empty(); + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/Attributes/ActionPriorityAttribute.cs b/src/Navigator/Actions/Attributes/ActionPriorityAttribute.cs new file mode 100644 index 0000000..fb2ee3b --- /dev/null +++ b/src/Navigator/Actions/Attributes/ActionPriorityAttribute.cs @@ -0,0 +1,15 @@ +namespace Navigator.Actions.Attributes; + +/// +/// Sets the priority of an action when multiple actions are available for the same trigger. +/// +[System.AttributeUsage(System.AttributeTargets.Class)] +public class ActionPriorityAttribute : System.Attribute +{ + public readonly ushort Priority; + + public ActionPriorityAttribute(ushort priority) + { + Priority = priority; + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/Attributes/ActionTypeAttribute.cs b/src/Navigator/Actions/Attributes/ActionTypeAttribute.cs new file mode 100644 index 0000000..3383e7e --- /dev/null +++ b/src/Navigator/Actions/Attributes/ActionTypeAttribute.cs @@ -0,0 +1,12 @@ +namespace Navigator.Actions.Attributes; + +[System.AttributeUsage(System.AttributeTargets.Class)] +public class ActionTypeAttribute : System.Attribute +{ + public string ActionType; + + public ActionTypeAttribute(string action) + { + ActionType = action; + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/BaseAction.cs b/src/Navigator/Actions/BaseAction.cs new file mode 100644 index 0000000..f934e85 --- /dev/null +++ b/src/Navigator/Actions/BaseAction.cs @@ -0,0 +1,36 @@ +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Actions; + +/// +/// Base action to use for any action. +/// +[ActionPriority(Actions.Priority.Default)] +public abstract class BaseAction : IAction +{ + /// + /// Used to access inside the action. + /// + protected readonly INavigatorContextAccessor NavigatorContextAccessor; + + /// + public virtual ushort Priority { get; protected set; } = Actions.Priority.Default; + + /// + /// Timestamp of the request on creation. + /// + public DateTime Timestamp { get; } + + /// + /// Default constructor for + /// + public BaseAction(INavigatorContextAccessor navigatorContextAccessor) + { + NavigatorContextAccessor = navigatorContextAccessor; + Timestamp = DateTime.UtcNow; + } + + /// + public abstract bool CanHandleCurrentContext(); +} \ No newline at end of file diff --git a/src/Navigator/Actions/IAction.cs b/src/Navigator/Actions/IAction.cs new file mode 100644 index 0000000..822533b --- /dev/null +++ b/src/Navigator/Actions/IAction.cs @@ -0,0 +1,15 @@ +using MediatR; + +namespace Navigator.Actions; + +/// +/// Base contract for an action. +/// +public interface IAction : IRequest +{ + /// + /// This function must return true when the incoming update can be handled by this action. + /// + /// + bool CanHandleCurrentContext(); +} \ No newline at end of file diff --git a/src/Navigator/Actions/IActionLauncher.cs b/src/Navigator/Actions/IActionLauncher.cs new file mode 100644 index 0000000..080895b --- /dev/null +++ b/src/Navigator/Actions/IActionLauncher.cs @@ -0,0 +1,6 @@ +namespace Navigator.Actions; + +public interface IActionLauncher +{ + Task Launch(); +} \ No newline at end of file diff --git a/src/Navigator/Actions/IActionMiddleware.cs b/src/Navigator/Actions/IActionMiddleware.cs new file mode 100644 index 0000000..6730605 --- /dev/null +++ b/src/Navigator/Actions/IActionMiddleware.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Navigator.Actions; + +/// +/// TODO +/// +/// +/// +public interface IActionMiddleware : IPipelineBehavior where TAction : IAction +{ +} \ No newline at end of file diff --git a/src/Navigator/Actions/NavigatorOptionsExtensions.cs b/src/Navigator/Actions/NavigatorOptionsExtensions.cs new file mode 100644 index 0000000..d4c5d67 --- /dev/null +++ b/src/Navigator/Actions/NavigatorOptionsExtensions.cs @@ -0,0 +1,132 @@ +using System.Collections.Immutable; +using Navigator.Actions.Attributes; +using Navigator.Configuration; + +namespace Navigator.Actions; + +public static class NavigatorOptionsExtensions +{ + #region NavigatorActionTypeCollection + + private const string NavigatorActionTypeCollection = "_navigator.options.action_type_collection"; + + /// + /// Pseudo-internal call, don't use it unless you know what you are doing. + /// + /// + /// + /// + public static void RegisterActionsCore(this NavigatorOptions navigatorOptions, IEnumerable actions, bool force = false) + { + var actionsDictionary = actions.GroupBy(type => type.GetActionType()) + .ToImmutableDictionary(types => types.Key, types => types.ToArray()); + + if (force) + { + navigatorOptions.ForceRegisterOption(NavigatorActionTypeCollection, actionsDictionary); + } + else + { + navigatorOptions.TryRegisterOption(NavigatorActionTypeCollection, actionsDictionary); + } + } + + /// + /// Pseudo-internal call, don't use it unless you know what you are doing. + /// + /// + /// + public static ImmutableDictionary RetrieveActions(this NavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption>(NavigatorActionTypeCollection) ?? ImmutableDictionary.Empty; + } + + #endregion + + #region NavigatorActionPriorityCollection + + private const string NavigatorActionPriorityCollection = "_navigator.options.action_priority_collection"; + + /// + /// Pseudo-internal call, don't use it unless you know what you are doing. + /// + /// + /// + /// + /// + public static void RegisterActionsPriorityCore(this NavigatorOptions navigatorOptions, IEnumerable actions, bool force = false) + { + var priorityTable = actions.ToImmutableDictionary( + type => type.FullName ?? string.Empty, + type => type.GetActionPriority()); + + if (force) + { + navigatorOptions.ForceRegisterOption(NavigatorActionPriorityCollection, priorityTable); + + } + else + { + navigatorOptions.TryRegisterOption(NavigatorActionPriorityCollection, priorityTable); + } + } + + /// + /// Pseudo-internal call, don't use it unless you know what you are doing. + /// + /// + /// + public static ImmutableDictionary RetrievePriorities(this NavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption>(NavigatorActionPriorityCollection) ?? ImmutableDictionary.Empty; + } + + #endregion + + internal static string GetActionType(this Type? type) + { + ActionTypeAttribute? actionTypeAttribute = default; + + if (type?.CustomAttributes.Any(data => data.AttributeType == typeof(ActionTypeAttribute)) ?? false) + { + actionTypeAttribute = Attribute.GetCustomAttribute(type, typeof(ActionTypeAttribute)) as ActionTypeAttribute; + + return actionTypeAttribute?.ActionType ?? string.Empty; + } + + if (type?.BaseType is not null && !type.BaseType.IsInterface) + { + var action = GetActionType(type.BaseType); + + if (action != string.Empty) + { + return action; + } + } + + if (type is not null) + { + actionTypeAttribute = Attribute.GetCustomAttribute(type, typeof(ActionTypeAttribute)) as ActionTypeAttribute; + } + + return actionTypeAttribute?.ActionType ?? string.Empty; + } + internal static ushort GetActionPriority(this Type? type) + { + if (type?.CustomAttributes.Any(data => data.AttributeType == typeof(ActionPriorityAttribute)) ?? false) + { + var priorityAttribute = (ActionPriorityAttribute) Attribute.GetCustomAttribute(type, typeof(ActionPriorityAttribute))!; + + return priorityAttribute.Priority; + } + + if (type?.BaseType is not null && !type.BaseType.IsInterface) + { + var priority = GetActionPriority(type.BaseType); + + return priority; + } + + return Priority.Default; + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/Priority.cs b/src/Navigator/Actions/Priority.cs new file mode 100644 index 0000000..7ddfa02 --- /dev/null +++ b/src/Navigator/Actions/Priority.cs @@ -0,0 +1,8 @@ +namespace Navigator.Actions; + +public static class Priority +{ + public const ushort Low = 15000; + public const ushort Default = 10000; + public const ushort High = 5000; +} \ No newline at end of file diff --git a/src/Navigator/Actions/ProviderAgnosticAction.cs b/src/Navigator/Actions/ProviderAgnosticAction.cs new file mode 100644 index 0000000..4244ee8 --- /dev/null +++ b/src/Navigator/Actions/ProviderAgnosticAction.cs @@ -0,0 +1,19 @@ +using Navigator.Actions.Attributes; +using Navigator.Context; + +namespace Navigator.Actions; + +/// +/// Base action for provider agnostic actions. +/// +[ActionType(nameof(ProviderAgnosticAction))] +public abstract class ProviderAgnosticAction : BaseAction +{ + /// + /// Default constructor. + /// + /// + protected ProviderAgnosticAction(INavigatorContextAccessor navigatorContextAccessor) : base(navigatorContextAccessor) + { + } +} \ No newline at end of file diff --git a/src/Navigator/Actions/Status.cs b/src/Navigator/Actions/Status.cs new file mode 100644 index 0000000..1bcd3d5 --- /dev/null +++ b/src/Navigator/Actions/Status.cs @@ -0,0 +1,14 @@ +namespace Navigator.Actions; + +public struct Status +{ + private readonly bool _isSuccess; + + public bool IsSuccess => _isSuccess; + public bool IsError => !_isSuccess; + + public Status(bool isSuccess) + { + _isSuccess = isSuccess; + } +} \ No newline at end of file diff --git a/src/Navigator/BotClient.cs b/src/Navigator/BotClient.cs deleted file mode 100644 index 65dce5b..0000000 --- a/src/Navigator/BotClient.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net.Http; -using Microsoft.Extensions.Options; -using MihaZupan.TelegramBotClients; -using Navigator.Abstractions; - -namespace Navigator -{ - /// - public class BotClient : RateLimitedTelegramBotClient, IBotClient - { - /// - public BotClient(NavigatorOptions options) : base(options.GetTelegramToken(), (HttpClient) default!, options.GetSchedulerSettingsOrDefault()) - { - } - } -} \ No newline at end of file diff --git a/src/Navigator/Configuration/EndpointRouteBuilderExtensions.cs b/src/Navigator/Configuration/EndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..18b7a0f --- /dev/null +++ b/src/Navigator/Configuration/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Routing; + +namespace Navigator.Configuration; + +public static class EndpointRouteBuilderExtensions +{ + /// + /// Configure navigator's provider's endpoints. + /// + /// + /// + public static NavigatorRouteConfiguration MapNavigator(this IEndpointRouteBuilder endpointRouteBuilder) + { + return new(endpointRouteBuilder); + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/Extension/NavigatorExtensionConfiguration.cs b/src/Navigator/Configuration/Extension/NavigatorExtensionConfiguration.cs new file mode 100644 index 0000000..1c5e63c --- /dev/null +++ b/src/Navigator/Configuration/Extension/NavigatorExtensionConfiguration.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Navigator.Configuration.Extension; + +/// +/// Provides a entry point for configuring new extensions for Navigator. +/// +public class NavigatorExtensionConfiguration +{ + private readonly NavigatorConfiguration _navigatorConfiguration; + + /// + /// Default constructor for . + /// + /// + public NavigatorExtensionConfiguration(NavigatorConfiguration navigatorConfiguration) + { + _navigatorConfiguration = navigatorConfiguration; + } + + /// + /// Configure a new extension using this method. + /// + /// + /// + public NavigatorConfiguration Extension(Action<(NavigatorOptions Options, IServiceCollection Services)> configuration) + { + configuration.Invoke((_navigatorConfiguration.Options, _navigatorConfiguration.Services)); + + _navigatorConfiguration.RegisterOrReplaceOptions(); + + return _navigatorConfiguration; + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/INavigatorOptions.cs b/src/Navigator/Configuration/INavigatorOptions.cs new file mode 100644 index 0000000..8a5ae78 --- /dev/null +++ b/src/Navigator/Configuration/INavigatorOptions.cs @@ -0,0 +1,13 @@ +namespace Navigator.Configuration; + +/// +/// Represents all the options you can use to configure Navigator. +/// +public interface INavigatorOptions +{ + bool TryRegisterOption(string key, object option); + void ForceRegisterOption(string key, object option); + TType? RetrieveOption(string key); + Dictionary RetrieveAllOptions(); + void Import(Dictionary options, bool overwrite = false); +} \ No newline at end of file diff --git a/src/Navigator/Configuration/NavigatorConfiguration.cs b/src/Navigator/Configuration/NavigatorConfiguration.cs new file mode 100644 index 0000000..3dd1465 --- /dev/null +++ b/src/Navigator/Configuration/NavigatorConfiguration.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Navigator.Configuration.Extension; +using Navigator.Configuration.Provider; + +namespace Navigator.Configuration; + +/// +/// Helper functions for configuring navigator services. +/// +public class NavigatorConfiguration +{ + public NavigatorProviderConfiguration WithProvider { get; internal set; } + + public NavigatorExtensionConfiguration WithExtension { get; internal set; } + + /// + /// Gets the that are being used. + /// + /// + /// The + /// + public NavigatorOptions Options { get; internal set; } + + /// + /// Gets the services are attached to. + /// + /// + /// The services are attached to. + /// + public IServiceCollection Services { get; internal set; } + + /// + /// Creates a new instance of . + /// + /// The to use. + /// The to attach to. + public NavigatorConfiguration(Action options, IServiceCollection services) + { + Options = new NavigatorOptions(); + options.Invoke(Options); + + Services = services; + + services.AddSingleton(Options); + + WithProvider = new NavigatorProviderConfiguration(this); + WithExtension = new NavigatorExtensionConfiguration(this); + } + + + public void RegisterOrReplaceOptions() + { + Services.Replace(ServiceDescriptor.Singleton(_ => Options)); + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/NavigatorOptions.cs b/src/Navigator/Configuration/NavigatorOptions.cs new file mode 100644 index 0000000..e3629b5 --- /dev/null +++ b/src/Navigator/Configuration/NavigatorOptions.cs @@ -0,0 +1,57 @@ +namespace Navigator.Configuration; + +/// +public class NavigatorOptions : INavigatorOptions +{ + private readonly Dictionary _options; + + public NavigatorOptions() + { + _options = new Dictionary(); + } + + public bool TryRegisterOption(string key, object option) + { + return _options.TryAdd(key, option); + } + + public void ForceRegisterOption(string key, object option) + { + _options.Remove(key); + + TryRegisterOption(key, option); + } + + public TType? RetrieveOption(string key) + { + if (_options.TryGetValue(key, out var option)) + { + return (TType) option; + } + + return default; + } + + public Dictionary RetrieveAllOptions() + { + return _options; + } + + public void Import(Dictionary options, bool overwrite = false) + { + if (overwrite) + { + foreach (var (key, option) in options) + { + ForceRegisterOption(key, option); + } + + return; + } + + foreach (var (key, option) in options) + { + TryRegisterOption(key, option); + } + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/NavigatorOptionsCollectionExtensions.cs b/src/Navigator/Configuration/NavigatorOptionsCollectionExtensions.cs new file mode 100644 index 0000000..3cc76cd --- /dev/null +++ b/src/Navigator/Configuration/NavigatorOptionsCollectionExtensions.cs @@ -0,0 +1,70 @@ +using System.Reflection; + +namespace Navigator.Configuration; + +/// +/// Navigator Configuration Options. +/// +public static class NavigatorOptionsCollectionExtensions +{ + #region WebHookBaseUrl + + private const string WebHookBaseUrlKey = "_navigator.options.webhook_base_url"; + + public static void SetWebHookBaseUrl(this NavigatorOptions navigatorOptions, string webHookBaseUrl) + { + navigatorOptions.TryRegisterOption(WebHookBaseUrlKey, webHookBaseUrl); + + } + + public static string? GetWebHookBaseUrl(this NavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption(WebHookBaseUrlKey); + } + + #endregion + + #region MultipleActions + + private const string MultipleActionsKey = "_navigator.options.multiple_actions"; + + public static void EnableMultipleActionsUsage(this NavigatorOptions navigatorOptions) + { + navigatorOptions.TryRegisterOption(MultipleActionsKey, true); + } + + public static bool MultipleActionsUsageIsEnabled(this NavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption(MultipleActionsKey); + } + + #endregion + + #region ActionsAssemblies + + private const string ActionsAssembliesKey = "_navigator.options.actions_assemblies"; + + public static void RegisterActionsFromAssemblies(this NavigatorOptions navigatorOptions, params Assembly[] assemblies) + { + var registeredAssemblies = navigatorOptions.RetrieveOption(ActionsAssembliesKey); + + if (registeredAssemblies?.Length > 0) + { + var combinedAssemblies = new List(registeredAssemblies); + combinedAssemblies.AddRange(assemblies); + + navigatorOptions.TryRegisterOption(ActionsAssembliesKey, combinedAssemblies); + } + else + { + navigatorOptions.TryRegisterOption(ActionsAssembliesKey, assemblies); + } + } + + public static Assembly[] GetActionsAssemblies(this NavigatorOptions navigatorOptions) + { + return navigatorOptions.RetrieveOption(ActionsAssembliesKey) ?? new[] { Assembly.GetCallingAssembly()}; + } + + #endregion +} \ No newline at end of file diff --git a/src/Navigator/Configuration/NavigatorRouteConfiguration.cs b/src/Navigator/Configuration/NavigatorRouteConfiguration.cs new file mode 100644 index 0000000..3b3d15c --- /dev/null +++ b/src/Navigator/Configuration/NavigatorRouteConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Routing; +using Navigator.Configuration.Provider; + +namespace Navigator.Configuration; + +public class NavigatorRouteConfiguration +{ + public IEndpointRouteBuilder EndpointRouteBuilder { get; internal set; } + + public NavigatorRouteProviderConfiguration ForProvider { get; internal set; } + + public NavigatorRouteConfiguration(IEndpointRouteBuilder endpointRouteBuilder) + { + EndpointRouteBuilder = endpointRouteBuilder; + + ForProvider = new NavigatorRouteProviderConfiguration(this); + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/Provider/NavigatorProviderConfiguration.cs b/src/Navigator/Configuration/Provider/NavigatorProviderConfiguration.cs new file mode 100644 index 0000000..7ea31de --- /dev/null +++ b/src/Navigator/Configuration/Provider/NavigatorProviderConfiguration.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Navigator.Configuration.Provider; + +/// +/// Provides a entry point for configuring new providers for Navigator. +/// +public class NavigatorProviderConfiguration +{ + private readonly NavigatorConfiguration _navigatorConfiguration; + + /// + /// Default constructor for . + /// + /// + public NavigatorProviderConfiguration(NavigatorConfiguration navigatorConfiguration) + { + _navigatorConfiguration = navigatorConfiguration; + } + + /// + /// Configure a new provider using this method. + /// + /// + /// + public NavigatorConfiguration Provider(Action<(NavigatorOptions Options, IServiceCollection Services)> configuration) + { + configuration.Invoke((_navigatorConfiguration.Options, _navigatorConfiguration.Services)); + + _navigatorConfiguration.RegisterOrReplaceOptions(); + + return _navigatorConfiguration; + } +} \ No newline at end of file diff --git a/src/Navigator/Configuration/Provider/NavigatorRouteProviderConfiguration.cs b/src/Navigator/Configuration/Provider/NavigatorRouteProviderConfiguration.cs new file mode 100644 index 0000000..2a29615 --- /dev/null +++ b/src/Navigator/Configuration/Provider/NavigatorRouteProviderConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Routing; + +namespace Navigator.Configuration.Provider; + +public class NavigatorRouteProviderConfiguration +{ + private readonly NavigatorRouteConfiguration _navigatorRouteConfiguration; + + public NavigatorRouteProviderConfiguration(NavigatorRouteConfiguration navigatorRouteConfiguration) + { + _navigatorRouteConfiguration = navigatorRouteConfiguration; + } + + public NavigatorRouteConfiguration Provider(Action routeActions) + { + routeActions.Invoke(_navigatorRouteConfiguration.EndpointRouteBuilder); + + return _navigatorRouteConfiguration; + } +} \ No newline at end of file diff --git a/src/Navigator/Context/Extensions/Bundled/OriginalEvent/NavigatorContextExtensions.cs b/src/Navigator/Context/Extensions/Bundled/OriginalEvent/NavigatorContextExtensions.cs new file mode 100644 index 0000000..07096ff --- /dev/null +++ b/src/Navigator/Context/Extensions/Bundled/OriginalEvent/NavigatorContextExtensions.cs @@ -0,0 +1,32 @@ +namespace Navigator.Context.Extensions.Bundled.OriginalEvent; + +public static class NavigatorContextExtensions +{ + #region OriginalEvent + + public static object? GetOriginalEvent(this INavigatorContext navigatorContext) + { + return navigatorContext.Extensions.GetValueOrDefault(OriginalEventContextExtension.OriginalEventKey); + } + + public static TEvent GetOriginalEvent(this INavigatorContext navigatorContext) where TEvent : class + { + var update = navigatorContext.GetOriginalEventOrDefault(); + + return update ?? throw new NavigatorException("Update was not found."); + } + + public static TEvent? GetOriginalEventOrDefault(this INavigatorContext navigatorContext) where TEvent : class + { + var @event = navigatorContext.Extensions.GetValueOrDefault(OriginalEventContextExtension.OriginalEventKey); + + if (@event is TEvent originalEvent) + { + return originalEvent; + } + + return default; + } + + #endregion +} \ No newline at end of file diff --git a/src/Navigator/Context/Extensions/Bundled/OriginalEvent/OriginalEventContextExtension.cs b/src/Navigator/Context/Extensions/Bundled/OriginalEvent/OriginalEventContextExtension.cs new file mode 100644 index 0000000..905a860 --- /dev/null +++ b/src/Navigator/Context/Extensions/Bundled/OriginalEvent/OriginalEventContextExtension.cs @@ -0,0 +1,13 @@ +namespace Navigator.Context.Extensions.Bundled.OriginalEvent; + +internal class OriginalEventContextExtension : INavigatorContextExtension +{ + public const string OriginalEventKey = "_navigator.extensions.original_event"; + + public Task Extend(INavigatorContext navigatorContext, INavigatorContextBuilderOptions builderOptions) + { + navigatorContext.Extensions.TryAdd(OriginalEventKey, builderOptions.GetOriginalEventOrDefault()); + + return Task.FromResult(navigatorContext); + } +} \ No newline at end of file diff --git a/src/Navigator/Context/Extensions/INavigatorContextExtension.cs b/src/Navigator/Context/Extensions/INavigatorContextExtension.cs new file mode 100644 index 0000000..674f97c --- /dev/null +++ b/src/Navigator/Context/Extensions/INavigatorContextExtension.cs @@ -0,0 +1,6 @@ +namespace Navigator.Context.Extensions; + +public interface INavigatorContextExtension +{ + Task Extend(INavigatorContext navigatorContext, INavigatorContextBuilderOptions builderOptions); +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContext.cs b/src/Navigator/Context/INavigatorContext.cs new file mode 100644 index 0000000..7c5a7ef --- /dev/null +++ b/src/Navigator/Context/INavigatorContext.cs @@ -0,0 +1,33 @@ +using Navigator.Entities; + +namespace Navigator.Context; + +public interface INavigatorContext +{ + /// + /// + /// + INavigatorProvider Provider { get; } + + /// + /// + /// + Bot BotProfile { get; } + + /// + /// + /// + Dictionary Extensions { get; } + + /// + /// + /// + Dictionary Items { get; } + + /// + /// + /// + public string ActionType { get; } + + public Conversation Conversation { get; } +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContextAccessor.cs b/src/Navigator/Context/INavigatorContextAccessor.cs new file mode 100644 index 0000000..4889463 --- /dev/null +++ b/src/Navigator/Context/INavigatorContextAccessor.cs @@ -0,0 +1,6 @@ +namespace Navigator.Context; + +public interface INavigatorContextAccessor +{ + INavigatorContext NavigatorContext { get; } +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContextBuilder.cs b/src/Navigator/Context/INavigatorContextBuilder.cs new file mode 100644 index 0000000..83b0cfa --- /dev/null +++ b/src/Navigator/Context/INavigatorContextBuilder.cs @@ -0,0 +1,6 @@ +namespace Navigator.Context; + +public interface INavigatorContextBuilder +{ + Task Build(Action configurationAction); +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContextBuilderConversationSource.cs b/src/Navigator/Context/INavigatorContextBuilderConversationSource.cs new file mode 100644 index 0000000..d0e1937 --- /dev/null +++ b/src/Navigator/Context/INavigatorContextBuilderConversationSource.cs @@ -0,0 +1,17 @@ +using Navigator.Entities; + +namespace Navigator.Context; + +/// +/// Defines a source for to use +/// while building a . +/// +public interface INavigatorContextBuilderConversationSource +{ + /// + /// Returns a in accordance to the original event that spawned this context. + /// + /// + /// + Task GetConversationAsync(object? originalEvent); +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContextBuilderOptions.cs b/src/Navigator/Context/INavigatorContextBuilderOptions.cs new file mode 100644 index 0000000..5022aac --- /dev/null +++ b/src/Navigator/Context/INavigatorContextBuilderOptions.cs @@ -0,0 +1,9 @@ +namespace Navigator.Context; + +public interface INavigatorContextBuilderOptions +{ + bool TryRegisterOption(string key, object option); + void ForceRegisterOption(string key, object option); + TType? RetrieveOption(string key); + Dictionary RetrieveAllOptions(); +} \ No newline at end of file diff --git a/src/Navigator/Context/INavigatorContextFactory.cs b/src/Navigator/Context/INavigatorContextFactory.cs new file mode 100644 index 0000000..d061c68 --- /dev/null +++ b/src/Navigator/Context/INavigatorContextFactory.cs @@ -0,0 +1,11 @@ +namespace Navigator.Context; + +/// +/// Factory for creating and retrieving implementations of +/// +public interface INavigatorContextFactory +{ + Task Supply(Action action); + + INavigatorContext Retrieve(); +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContext.cs b/src/Navigator/Context/NavigatorContext.cs new file mode 100644 index 0000000..5e887af --- /dev/null +++ b/src/Navigator/Context/NavigatorContext.cs @@ -0,0 +1,24 @@ +using Navigator.Entities; + +namespace Navigator.Context; + +internal class NavigatorContext : INavigatorContext +{ + public INavigatorProvider Provider { get; } + public Bot BotProfile { get; } + public Dictionary Extensions { get; } + public Dictionary Items { get; } + public string ActionType { get; } + public Conversation Conversation { get; } + + public NavigatorContext(INavigatorProvider provider, Bot botProfile, string actionType, Conversation conversation) + { + Provider = provider; + BotProfile = botProfile; + ActionType = actionType; + Conversation = conversation; + + Items = new Dictionary(); + Extensions = new Dictionary(); + } +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContextAccessor.cs b/src/Navigator/Context/NavigatorContextAccessor.cs new file mode 100644 index 0000000..68b7ff5 --- /dev/null +++ b/src/Navigator/Context/NavigatorContextAccessor.cs @@ -0,0 +1,13 @@ +namespace Navigator.Context; + +internal class NavigatorContextAccessor : INavigatorContextAccessor +{ + private readonly INavigatorContextFactory _navigatorContextFactory; + + public NavigatorContextAccessor(INavigatorContextFactory navigatorContextFactory) + { + _navigatorContextFactory = navigatorContextFactory; + } + + public INavigatorContext NavigatorContext => _navigatorContextFactory.Retrieve(); +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContextBuilder.cs b/src/Navigator/Context/NavigatorContextBuilder.cs new file mode 100644 index 0000000..f6d6bd7 --- /dev/null +++ b/src/Navigator/Context/NavigatorContextBuilder.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using Navigator.Context.Extensions; + +namespace Navigator.Context; + +internal class NavigatorContextBuilder : INavigatorContextBuilder +{ + private readonly ILogger _logger; + private readonly IEnumerable _navigatorProviders; + private readonly IEnumerable _navigatorContextExtensions; + private readonly INavigatorContextBuilderOptions _options; + private readonly INavigatorContextBuilderConversationSource _conversationSource; + + public NavigatorContextBuilder(ILogger logger, IEnumerable navigatorClients, IEnumerable navigatorContextExtensions, INavigatorContextBuilderConversationSource conversationSource) + { + _logger = logger; + _navigatorProviders = navigatorClients; + _navigatorContextExtensions = navigatorContextExtensions; + _conversationSource = conversationSource; + + _options = new NavigatorContextBuilderOptions(); + } + + public async Task Build(Action optionsAction) + { + optionsAction.Invoke(_options); + + var provider = _navigatorProviders.FirstOrDefault(p => p.GetType() == _options.GetProvider()); + + if (provider is null) + { + _logger.LogError("No provider found for: {@Provider}", _options.GetProvider()); + //TODO: make NavigatorException + throw new Exception($"No provider found for: {_options.GetProvider()?.Name}"); + } + + var actionType = _options.GetAcitonType() ?? throw new InvalidOperationException(); + + var conversation = await _conversationSource.GetConversationAsync(_options.GetOriginalEventOrDefault()); + + INavigatorContext context = new NavigatorContext(provider, await provider.GetClient().GetProfile(), actionType, conversation); + + foreach (var contextExtension in _navigatorContextExtensions) + { + context = await contextExtension.Extend(context, _options); + } + + return context; + } +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContextBuilderOptions.cs b/src/Navigator/Context/NavigatorContextBuilderOptions.cs new file mode 100644 index 0000000..503f406 --- /dev/null +++ b/src/Navigator/Context/NavigatorContextBuilderOptions.cs @@ -0,0 +1,38 @@ +namespace Navigator.Context; + +public class NavigatorContextBuilderOptions : INavigatorContextBuilderOptions +{ + private readonly Dictionary _options; + + public NavigatorContextBuilderOptions() + { + _options = new Dictionary(); + } + + public bool TryRegisterOption(string key, object option) + { + return _options.TryAdd(key, option); + } + + public void ForceRegisterOption(string key, object option) + { + _options.Remove(key); + + TryRegisterOption(key, option); + } + + public TType? RetrieveOption(string key) + { + if (_options.TryGetValue(key, out var option)) + { + return (TType) option; + } + + return default; + } + + public Dictionary RetrieveAllOptions() + { + return _options; + } +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContextBuilderOptionsExtensions.cs b/src/Navigator/Context/NavigatorContextBuilderOptionsExtensions.cs new file mode 100644 index 0000000..3d26233 --- /dev/null +++ b/src/Navigator/Context/NavigatorContextBuilderOptionsExtensions.cs @@ -0,0 +1,55 @@ +namespace Navigator.Context; + +public static class NavigatorContextBuilderOptionsExtensions +{ + #region Provider + + private const string ProviderKey = "_navigator.context.options.provider"; + + public static void SetProvider(this INavigatorContextBuilderOptions contextBuilderOptions) where TProvider : INavigatorProvider + { + contextBuilderOptions.TryRegisterOption(ProviderKey, typeof(TProvider)); + + } + + public static Type? GetProvider(this INavigatorContextBuilderOptions contextBuilderOptions) + { + return contextBuilderOptions.RetrieveOption(ProviderKey); + } + + #endregion + + #region ActionType + + private const string ActionTypeKey = "_navigator.context.options.action_type"; + + public static void SetActionType(this INavigatorContextBuilderOptions contextBuilderOptions, string actionType) + { + contextBuilderOptions.TryRegisterOption(ActionTypeKey, actionType); + + } + + public static string? GetAcitonType(this INavigatorContextBuilderOptions contextBuilderOptions) + { + return contextBuilderOptions.RetrieveOption(ActionTypeKey); + } + + #endregion + + #region OriginalUpdate + + private const string OriginalEventKey = "_navigator.context.options.original_event"; + + public static void SetOriginalEvent(this INavigatorContextBuilderOptions contextBuilderOptions, object originalUpdate) + { + contextBuilderOptions.TryRegisterOption(OriginalEventKey, originalUpdate); + + } + + public static object? GetOriginalEventOrDefault(this INavigatorContextBuilderOptions contextBuilderOptions) + { + return contextBuilderOptions.RetrieveOption(OriginalEventKey); + } + + #endregion +} \ No newline at end of file diff --git a/src/Navigator/Context/NavigatorContextFactory.cs b/src/Navigator/Context/NavigatorContextFactory.cs new file mode 100644 index 0000000..85f8285 --- /dev/null +++ b/src/Navigator/Context/NavigatorContextFactory.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; + +namespace Navigator.Context; + +internal class NavigatorContextFactory : INavigatorContextFactory +{ + private readonly ILogger _logger; + private readonly INavigatorContextBuilder _navigatorContextBuilder; + private INavigatorContext? NavigatorContext { get; set; } + + public NavigatorContextFactory(ILogger logger, INavigatorContextBuilder navigatorContextBuilder) + { + _logger = logger; + _navigatorContextBuilder = navigatorContextBuilder; + } + + public async Task Supply(Action action) + { + try + { + _logger.LogTrace("Building a new NavigatorContext"); + + NavigatorContext = await _navigatorContextBuilder.Build(action); + } + catch (Exception e) + { + _logger.LogError(e, "Unhandled exception building NavigatorContext"); + throw; + } + } + + public INavigatorContext Retrieve() + { + return NavigatorContext ?? throw new NullReferenceException(); + } +} \ No newline at end of file diff --git a/src/Navigator/EndpointRouteBuilderExtensions.cs b/src/Navigator/EndpointRouteBuilderExtensions.cs deleted file mode 100644 index 2bb80e4..0000000 --- a/src/Navigator/EndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Navigator.Abstractions; - -namespace Navigator -{ - public static class EndpointRouteBuilderExtensions - { - public static void MapNavigator(this IEndpointRouteBuilder endpointRouteBuilder) - { - using var scope = endpointRouteBuilder.ServiceProvider.CreateScope(); - - var options = scope.ServiceProvider.GetRequiredService(); - - endpointRouteBuilder.MapPost(options.GetWebHookEndpointOrDefault(), ProcessTelegramUpdate); - } - - private static async Task ProcessTelegramUpdate(HttpContext context) - { - context.Response.StatusCode = 200; - - if (context.Request.ContentType == "application/json") - { - var navigatorMiddleware = context.RequestServices.GetRequiredService(); - - await navigatorMiddleware.Handle(context.Request); - } - } - } -} \ No newline at end of file diff --git a/src/Navigator/Entities/Bot.cs b/src/Navigator/Entities/Bot.cs new file mode 100644 index 0000000..b3000b1 --- /dev/null +++ b/src/Navigator/Entities/Bot.cs @@ -0,0 +1,20 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Navigator.Entities; + +public abstract class Bot +{ + protected Bot(string input) + { + Id = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(input)).Take(16).ToArray()); + } + + /// + /// Id of the bot. + /// + /// Generally a deterministic Guid based on some kind of input. + /// + /// + public Guid Id { get; init; } +} \ No newline at end of file diff --git a/src/Navigator/Entities/Chat.cs b/src/Navigator/Entities/Chat.cs new file mode 100644 index 0000000..3135045 --- /dev/null +++ b/src/Navigator/Entities/Chat.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Navigator.Entities; + +/// +/// Represents a chat. +/// +public abstract class Chat +{ + protected Chat() + { + } + + protected Chat(string input) + { + Id = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(input)).Take(16).ToArray()); + } + + /// + /// Id of the chat. + /// + /// Generally a deterministic Guid based on some kind of input. + /// + /// + public Guid Id { get; init; } +} \ No newline at end of file diff --git a/src/Navigator/Entities/Conversation.cs b/src/Navigator/Entities/Conversation.cs new file mode 100644 index 0000000..e2a27ac --- /dev/null +++ b/src/Navigator/Entities/Conversation.cs @@ -0,0 +1,34 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Navigator.Entities; + +/// +/// Represents an interaction between a user and chat. +/// +public abstract class Conversation +{ + protected Conversation() + { + } + + protected Conversation(User user, Chat? chat) + { + Id = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes($"{user.Id}+{chat?.Id}")).Take(16).ToArray()); + + User = user; + Chat = chat; + } + + public Guid Id { get; set; } + + /// + /// User + /// + public User User { get; set; } + + /// + /// Chat + /// + public Chat? Chat { get; set; } +} \ No newline at end of file diff --git a/src/Navigator/Entities/User.cs b/src/Navigator/Entities/User.cs new file mode 100644 index 0000000..902c3da --- /dev/null +++ b/src/Navigator/Entities/User.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Navigator.Entities; + +/// +/// Represents a user. +/// +public abstract class User +{ + protected User() + { + } + + protected User(string input) + { + Id = new Guid(SHA256.HashData(Encoding.UTF8.GetBytes(input)).Take(16).ToArray()); + } + + /// + /// Id of the user. + /// + /// Generally a deterministic Guid based on some kind of input. + /// + /// + public Guid Id { get; init; } +} \ No newline at end of file diff --git a/src/Navigator/Hosted/SetTelegramBotWebHookHostedService.cs b/src/Navigator/Hosted/SetTelegramBotWebHookHostedService.cs deleted file mode 100644 index 56634ed..0000000 --- a/src/Navigator/Hosted/SetTelegramBotWebHookHostedService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Navigator.Abstractions; - -namespace Navigator.Hosted -{ - /// - /// WebHook service for Navigator. - /// - public class SetTelegramBotWebHookHostedService : BackgroundService - { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly string _webHookUrl; - - /// - /// Default constructor. - /// - /// - /// - /// - /// - public SetTelegramBotWebHookHostedService(ILogger logger, IServiceScopeFactory serviceScopeFactory, - NavigatorOptions navigatorOptions) - { - _logger = logger; - _serviceScopeFactory = serviceScopeFactory; - - if (string.IsNullOrWhiteSpace(navigatorOptions.GetWebHookBaseUrl())) - { - throw new ArgumentNullException(nameof(navigatorOptions), "An URL for WebHook is required."); - } - - _webHookUrl = $"{navigatorOptions.GetWebHookBaseUrl()}/{navigatorOptions.GetWebHookEndpointOrDefault()}"; - } - - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogTrace("Starting with setup of webhook."); - _logger.LogTrace("Using webHook url {WebHookUrl}", _webHookUrl); - - using var scope = _serviceScopeFactory.CreateScope(); - - var botClient = scope.ServiceProvider.GetRequiredService(); - - await botClient.SetWebhookAsync(_webHookUrl, cancellationToken: stoppingToken); - - var me = await botClient.GetMeAsync(stoppingToken); - - _logger.LogInformation($"Telegram Bot Client is receiving updates for bot: @{me.Username} at the url: {_webHookUrl}"); - } - } -} \ No newline at end of file diff --git a/src/Navigator/INavigatorClient.cs b/src/Navigator/INavigatorClient.cs new file mode 100644 index 0000000..49e218a --- /dev/null +++ b/src/Navigator/INavigatorClient.cs @@ -0,0 +1,13 @@ +using Navigator.Entities; + +namespace Navigator; + +public interface INavigatorClient +{ + /// + /// Retrieves the bot user information. + /// + /// + /// + Task GetProfile(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Navigator/INavigatorProvider.cs b/src/Navigator/INavigatorProvider.cs new file mode 100644 index 0000000..674ed60 --- /dev/null +++ b/src/Navigator/INavigatorProvider.cs @@ -0,0 +1,18 @@ +namespace Navigator; + +/// +/// Represents a provider for Navigator. +/// +public interface INavigatorProvider +{ + public string Name { get; init; } + + /// + /// Gets the specific client for this provider. + /// + /// An implementation of + INavigatorClient GetClient(); + + + Type GetConversationType(); +} \ No newline at end of file diff --git a/src/Navigator/Navigator.csproj b/src/Navigator/Navigator.csproj index f46f982..46ac8f8 100644 --- a/src/Navigator/Navigator.csproj +++ b/src/Navigator/Navigator.csproj @@ -1,44 +1,37 @@  - net5.0 - 9 - true + net6.0 + true + enable true - 0.9.102-beta - Navigator - Navigator Framework + Navigator Lucas Maximiliano Marino - A highly opinionated telegram bot framework, mainly based on Telegram.Bot. + A highly opinionated universal bot framework for .NET https://github.com/navigatorframework/navigator - https://github.com/navigatorframework/navigator/blob/master/LICENSE https://github.com/navigatorframework/navigator + https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png + README.md Telegram, Bot, Framework, Navigator true - Copyright © Lucas Maximiliano Marino 2021 - https://raw.githubusercontent.com/navigatorframework/navigator/master/assets/logo.png - enable + AGPL-3.0-only + Copyright © Lucas Maximiliano Marino 2022 - + - + - + - - - - + - - - - - + + + + + + - - - diff --git a/src/Navigator/NavigatorBuilder.cs b/src/Navigator/NavigatorBuilder.cs deleted file mode 100644 index 7b4463c..0000000 --- a/src/Navigator/NavigatorBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Navigator.Abstractions; -using Scrutor; - -namespace Navigator -{ - /// - /// Helper functions for configuring navigator services. - /// - public class NavigatorBuilder - { - /// - /// Gets the that are being used. - /// - /// - /// The - /// - public NavigatorOptions Options { get; private set; } - - /// - /// Gets the services are attached to. - /// - /// - /// The services are attached to. - /// - public IServiceCollection Services { get; private set; } - - /// - /// Creates a new instance of . - /// - /// The to use. - /// The to attach to. - - public NavigatorBuilder(Action options, IServiceCollection services) - { - Options = new NavigatorOptions(); - options.Invoke(Options); - - Services = services; - - services.AddSingleton(Options); - } - - public void RegisterOrReplaceOptions() - { - Services.Replace(ServiceDescriptor.Singleton(Options)); - } - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorContext.cs b/src/Navigator/NavigatorContext.cs deleted file mode 100644 index eba72fc..0000000 --- a/src/Navigator/NavigatorContext.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using Navigator.Abstractions; -using Telegram.Bot.Types; - -namespace Navigator -{ - /// - /// Default implementation of INavigatorContext. - /// - public class NavigatorContext : INavigatorContext - { - /// - /// Extensions. - /// - protected IDictionary Extensions { get; set; } = null!; - - /// - public IBotClient Client { get; } - - /// - public Dictionary Items { get; } - - /// - public User BotProfile { get; protected set; } = null!; - - /// - public Update Update { get; protected set; } = null!; - - /// - /// Default constructor. - /// - /// - public NavigatorContext(IBotClient client) - { - Client = client; - Items = new Dictionary(); - } - - /// - public async Task Init(Update update, Dictionary extensions) - { - Update = update; - BotProfile = await Client.GetMeAsync(); - Extensions = new ReadOnlyDictionary(extensions); - } - - /// - public TExtension? Get(string extensionKey, bool throwIfNotFound = false) - { - if (Extensions.TryGetValue(extensionKey, out var extension)) - { - if (extension is TExtension castedExtension) - { - return castedExtension; - } - } - - return throwIfNotFound - ? throw new KeyNotFoundException($"{typeof(TExtension).Name} was not found.") - : (TExtension) default; - } - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorContextBuilder.cs b/src/Navigator/NavigatorContextBuilder.cs deleted file mode 100644 index 958503a..0000000 --- a/src/Navigator/NavigatorContextBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Telegram.Bot.Types; - -namespace Navigator -{ - public class NavigatorContextBuilder : INavigatorContextBuilder - { - protected readonly ILogger Logger; - protected readonly IEnumerable ExtensionProviders; - protected readonly INavigatorContext Ctx; - public NavigatorContextBuilder(ILogger logger, IEnumerable extensionProviders, INavigatorContext ctx) - { - Logger = logger; - ExtensionProviders = extensionProviders.OrderBy(ep => ep.Order); - Ctx = ctx; - } - - public async Task Build(Update update) - { - var extensions = new Dictionary(); - - foreach (var extensionProvider in ExtensionProviders) - { - var (type, extension) = await extensionProvider.Process(update); - - if (type != null && extension != null) - { - extensions.Add(type, extension); - } - } - - await Ctx.Init(update, extensions); - } - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorException.cs b/src/Navigator/NavigatorException.cs new file mode 100644 index 0000000..92fd54d --- /dev/null +++ b/src/Navigator/NavigatorException.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; + +namespace Navigator; + +/// +/// TODO +/// +public class NavigatorException : Exception +{ + /// + public NavigatorException() + { + } + + /// + protected NavigatorException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + /// + public NavigatorException(string? message) : base(message) + { + } + + /// + public NavigatorException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Navigator/NavigatorMiddleware.cs b/src/Navigator/NavigatorMiddleware.cs deleted file mode 100644 index ffa77eb..0000000 --- a/src/Navigator/NavigatorMiddleware.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Newtonsoft.Json; -using Telegram.Bot.Types; - -namespace Navigator -{ - /// - /// - /// - public class NavigatorMiddleware : INavigatorMiddleware - { - /// - /// Logger. - /// - protected readonly ILogger Logger; - - /// - /// Notification Launcher. - /// - protected readonly INotificationLauncher NotificationLauncher; - - /// - /// Action Launcher. - /// - protected readonly IActionLauncher ActionLauncher; - - /// - /// Context Builder. - /// - protected readonly INavigatorContextBuilder NavigatorContextBuilder; - - /// - /// Default constructor. - /// - /// - /// - /// - /// - public NavigatorMiddleware(ILogger logger, INotificationLauncher notificationLauncher, IActionLauncher actionLauncher, INavigatorContextBuilder navigatorContextBuilder) - { - Logger = logger; - NotificationLauncher = notificationLauncher; - ActionLauncher = actionLauncher; - NavigatorContextBuilder = navigatorContextBuilder; - } - - /// - public async Task Handle(HttpRequest httpRequest) - { - Logger.LogTrace("Parsing telegram update."); - var update = await ParseTelegramUpdate(httpRequest.Body); - - if (update == null) - { - Logger.LogInformation("Telegram update was not parsed."); - return; - } - - Logger.LogTrace("Parsed telegram update with id: {UpdateId}", update.Id); - - await NavigatorContextBuilder.Build(update); - await NotificationLauncher.Launch(); - await ActionLauncher.Launch(); - } - - private static async Task ParseTelegramUpdate(Stream stream) - { - try - { - var reader = new StreamReader(stream); - var update = JsonConvert.DeserializeObject(await reader.ReadToEndAsync()); - - // TODO: Activate this when Telegram.Bot supports System.Text.Json - // update = await JsonSerializer.DeserializeAsync(stream); - - return update.Id == default ? default : update; - } - catch - { - return default; - } - } - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorOptions.cs b/src/Navigator/NavigatorOptions.cs deleted file mode 100644 index 4591fc8..0000000 --- a/src/Navigator/NavigatorOptions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using Navigator.Abstractions; - -namespace Navigator -{ - /// - /// Represents all the options you can use to configure the navigator framework. - /// - public class NavigatorOptions : INavigatorOptions - { - private readonly Dictionary _options; - - public NavigatorOptions() - { - _options = new Dictionary(); - } - - public bool TryRegisterOption(string key, object option) - { - return _options.TryAdd(key, option); - } - - public bool ForceRegisterOption(string key, object option) - { - _options.Remove(key); - - return TryRegisterOption(key, option); - } - - public TType? RetrieveOption(string key) - { - if (_options.TryGetValue(key, out var option)) - { - return (TType) option; - } - - return default; - } - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorOptionsCollectionExtensions.cs b/src/Navigator/NavigatorOptionsCollectionExtensions.cs deleted file mode 100644 index dbf9c17..0000000 --- a/src/Navigator/NavigatorOptionsCollectionExtensions.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using MihaZupan.TelegramBotClients.RateLimitedClient; - -namespace Navigator -{ - /// - /// - /// - public static class NavigatorOptionsCollectionExtensions - { - #region SchedulerSettings - - private const string SchedulerSettingsKey = "_navigator.options.scheduler_settings"; - - public static void SetSchedulerSettings(this NavigatorOptions navigatorOptions, SchedulerSettings schedulerSettings) - { - navigatorOptions.TryRegisterOption(SchedulerSettingsKey, schedulerSettings); - } - - public static SchedulerSettings GetSchedulerSettingsOrDefault(this NavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(SchedulerSettingsKey) ?? SchedulerSettings.Default; - } - - #endregion - - #region TelegramToken - - private const string TelegramTokenKey = "_navigator.options.telegram_token"; - - public static void SetTelegramToken(this NavigatorOptions navigatorOptions, string telegramToken) - { - navigatorOptions.TryRegisterOption(TelegramTokenKey, telegramToken); - - } - - public static string? GetTelegramToken(this NavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(TelegramTokenKey); - } - - #endregion - - #region WebHookBaseUrl - - private const string WebHookBaseUrlKey = "_navigator.options.webhook_base_url"; - - public static void SetWebHookBaseUrl(this NavigatorOptions navigatorOptions, string webHookBaseUrl) - { - navigatorOptions.TryRegisterOption(WebHookBaseUrlKey, webHookBaseUrl); - - } - - public static string? GetWebHookBaseUrl(this NavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(WebHookBaseUrlKey); - } - - #endregion - - #region WebHookEndpoint - - private const string WebHookEndpointKey = "_navigator.options.webhook_endpoint"; - - public static void SetWebHookEndpoint(this NavigatorOptions navigatorOptions, string webHookEndpoint) - { - navigatorOptions.TryRegisterOption(WebHookEndpointKey, webHookEndpoint); - } - - public static string GetWebHookEndpointOrDefault(this NavigatorOptions navigatorOptions) - { - var webHookEndpoint = navigatorOptions.RetrieveOption(WebHookEndpointKey); - - if (webHookEndpoint is null) - { - navigatorOptions.SetWebHookEndpoint($"bot/{Guid.NewGuid()}"); - } - - return navigatorOptions.RetrieveOption(WebHookEndpointKey)!; - } - - #endregion - - #region MultipleActions - - private const string MultipleActionsKey = "_navigator.options.multiple_actions"; - - public static void EnableMultipleActionsUsage(this NavigatorOptions navigatorOptions) - { - navigatorOptions.TryRegisterOption(MultipleActionsKey, true); - } - - public static bool MultipleActionsUsageIsEnabled(this NavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(MultipleActionsKey); - } - - #endregion - - #region ActionsAssemblies - - private const string ActionsAssembliesKey = "_navigator.options.actions_assemblies"; - - public static void RegisterActionsFromAssemblies(this NavigatorOptions navigatorOptions, params Assembly[] assemblies) - { - var registeredAssemblies = navigatorOptions.RetrieveOption(ActionsAssembliesKey); - - if (registeredAssemblies?.Length > 0) - { - var combinedAssemblies = new List(registeredAssemblies); - combinedAssemblies.AddRange(assemblies); - - navigatorOptions.TryRegisterOption(ActionsAssembliesKey, combinedAssemblies); - } - else - { - navigatorOptions.TryRegisterOption(ActionsAssembliesKey, assemblies); - } - } - - public static Assembly[] GetActionsAssemblies(this NavigatorOptions navigatorOptions) - { - return navigatorOptions.RetrieveOption(ActionsAssembliesKey) ?? new[] { Assembly.GetCallingAssembly()}; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Navigator/NavigatorServiceCollectionExtensions.cs b/src/Navigator/NavigatorServiceCollectionExtensions.cs deleted file mode 100644 index fd69b17..0000000 --- a/src/Navigator/NavigatorServiceCollectionExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Navigator.Abstractions; -using Navigator.Hosted; -using Scrutor; - -namespace Navigator -{ - public static class NavigatorServiceCollectionExtensions - { - public static NavigatorBuilder AddNavigator(this IServiceCollection services, Action options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options), "Navigator options are required for navigator framework to work."); - } - - var navigatorBuilder = new NavigatorBuilder(options, services); - - services.AddSingleton(); - - services.AddHostedService(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.Scan(scan => scan - .FromAssemblies(navigatorBuilder.Options.GetActionsAssemblies()) - .AddClasses(classes => classes.AssignableTo()) - .UsingRegistrationStrategy(RegistrationStrategy.Append) - .AsImplementedInterfaces() - .WithScopedLifetime()); - - navigatorBuilder.RegisterOrReplaceOptions(); - - return navigatorBuilder; - } - } -} \ No newline at end of file diff --git a/src/Navigator/Notification/NavigatorNotification.cs b/src/Navigator/Notification/NavigatorNotification.cs deleted file mode 100644 index 7a9d29d..0000000 --- a/src/Navigator/Notification/NavigatorNotification.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using Navigator.Abstractions; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Payments; - -namespace Navigator.Notification -{ - public abstract class NavigatorNotification : INavigatorNotification - { - public DateTime Timestamp { get; } - public int UpdateId { get; } - - protected NavigatorNotification(Update update) - { - UpdateId = update.Id; - Timestamp = DateTime.UtcNow; - } - } - - public class MessageNotification : NavigatorNotification - { - public readonly Message Message; - - public MessageNotification(Update update) : base(update) - { - Message = update.Message; - } - } - - public class InlineQueryNotification : NavigatorNotification - { - public readonly InlineQuery InlineQuery; - - public InlineQueryNotification(Update update) : base(update) - { - InlineQuery = update.InlineQuery; - } - } - - public class ChosenInlineResultNotification : NavigatorNotification - { - public readonly ChosenInlineResult ChosenInlineResult; - - public ChosenInlineResultNotification(Update update) : base(update) - { - ChosenInlineResult = update.ChosenInlineResult; - } - } - - public class CallbackQueryNotification : NavigatorNotification - { - public readonly CallbackQuery CallbackQuery; - - public CallbackQueryNotification(Update update) : base(update) - { - CallbackQuery = update.CallbackQuery; - } - } - - public class EditedMessageNotification : NavigatorNotification - { - public readonly Message EditedMessage; - - public EditedMessageNotification(Update update) : base(update) - { - EditedMessage = update.EditedMessage; - } - } - - public class ChannelPostNotification : NavigatorNotification - { - public readonly Message ChannelPost; - - public ChannelPostNotification(Update update) : base(update) - { - ChannelPost = update.ChannelPost; - } - } - - public class EditedChannelPostNotification : NavigatorNotification - { - public readonly Message EditedChannelPost; - - public EditedChannelPostNotification(Update update) : base(update) - { - EditedChannelPost = update.EditedChannelPost; - } - } - - public class ShippingQueryNotification : NavigatorNotification - { - public readonly ShippingQuery ShippingQuery; - - public ShippingQueryNotification(Update update) : base(update) - { - ShippingQuery = update.ShippingQuery; - } - } - - public class PreCheckoutQueryNotification : NavigatorNotification - { - public readonly PreCheckoutQuery PreCheckoutQuery; - - public PreCheckoutQueryNotification(Update update) : base(update) - { - PreCheckoutQuery = update.PreCheckoutQuery; - } - } - - public class PollNotification : NavigatorNotification - { - public readonly Poll Poll; - - public PollNotification(Update update) : base(update) - { - Poll = update.Poll; - } - } -} \ No newline at end of file diff --git a/src/Navigator/NotificationLauncher.cs b/src/Navigator/NotificationLauncher.cs deleted file mode 100644 index d4503fd..0000000 --- a/src/Navigator/NotificationLauncher.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading.Tasks; -using MediatR; -using Microsoft.Extensions.Logging; -using Navigator.Abstractions; -using Navigator.Notification; -using Telegram.Bot.Types.Enums; - -namespace Navigator -{ - public class NotificationLauncher : INotificationLauncher - { - protected readonly ILogger Logger; - protected readonly INavigatorContext Ctx; - protected readonly IMediator Mediator; - - public NotificationLauncher(ILogger logger, INavigatorContext navigatorContext, IMediator mediator) - { - Logger = logger; - Ctx = navigatorContext; - Mediator = mediator; - } - - public async Task Launch() - { - Logger.LogTrace("Starting with notification launch for update {UpdateId}", Ctx.Update.Id); - - if (Ctx.Update.Type == UpdateType.Message) - await Mediator.Publish(new MessageNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.InlineQuery) - await Mediator.Publish(new InlineQueryNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.ChosenInlineResult) - await Mediator.Publish(new ChosenInlineResultNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.CallbackQuery) - await Mediator.Publish(new CallbackQueryNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.EditedMessage) - await Mediator.Publish(new EditedMessageNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.ChannelPost) - await Mediator.Publish(new ChannelPostNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.EditedChannelPost) - await Mediator.Publish(new EditedChannelPostNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.ShippingQuery) - await Mediator.Publish(new ShippingQueryNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.PreCheckoutQuery) - await Mediator.Publish(new PreCheckoutQueryNotification(Ctx.Update)); - else if (Ctx.Update.Type == UpdateType.Poll) - await Mediator.Publish(new PollNotification(Ctx.Update)); - else - Logger.LogInformation("Unknown update type received"); - } - } -} \ No newline at end of file diff --git a/src/Navigator/Replies/ReplyBuilderOptions.cs b/src/Navigator/Replies/ReplyBuilderOptions.cs new file mode 100644 index 0000000..79009b7 --- /dev/null +++ b/src/Navigator/Replies/ReplyBuilderOptions.cs @@ -0,0 +1,6 @@ +namespace Navigator.Replies; + +public class ReplyBuilderOptions +{ + +} \ No newline at end of file diff --git a/src/Navigator/ServiceCollectionExtensions.cs b/src/Navigator/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..774184f --- /dev/null +++ b/src/Navigator/ServiceCollectionExtensions.cs @@ -0,0 +1,57 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Navigator.Actions; +using Navigator.Configuration; +using Navigator.Context; +using Navigator.Context.Extensions; +using Navigator.Context.Extensions.Bundled.OriginalEvent; +using Scrutor; + +namespace Navigator; + +public static class ServiceCollectionExtensions +{ + public static NavigatorConfiguration AddNavigator(this IServiceCollection services, Action options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options), "Navigator options are required for navigator framework to work."); + } + + var navigatorBuilder = new NavigatorConfiguration(options, services); + + services.AddNavigatorContextServices(); + + services.AddScoped(); + + services.AddScoped(); + + services.AddMediatR(navigatorBuilder.Options.GetActionsAssemblies()); + + services.Scan(scan => scan + .FromAssemblies(navigatorBuilder.Options.GetActionsAssemblies()) + .AddClasses(classes => classes.AssignableTo()) + .UsingRegistrationStrategy(RegistrationStrategy.Append) + .AsSelf() + .WithScopedLifetime()); + + navigatorBuilder.Options.RegisterActionsCore(services + .Where(descriptor => descriptor.ImplementationType?.IsAssignableTo(typeof(IAction)) ?? false) + .Select(descriptor => descriptor.ImplementationType!)); + + navigatorBuilder.Options.RegisterActionsPriorityCore(services + .Where(descriptor => descriptor.ImplementationType?.IsAssignableTo(typeof(IAction)) ?? false) + .Select(descriptor => descriptor.ImplementationType!)); + + navigatorBuilder.RegisterOrReplaceOptions(); + + return navigatorBuilder; + } + + internal static void AddNavigatorContextServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + } +} \ No newline at end of file