From 40c005e50e33701cafab4d639d377a28e4bec47a Mon Sep 17 00:00:00 2001 From: Marat Budkevich <93652988+marat2509@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:44:01 +0300 Subject: [PATCH] feat(release): v2.1.0 --- .github/workflows/docker.yml | 58 +++++++ .github/workflows/dotnetcore.yml | 9 +- .github/workflows/release.yml | 4 +- BridgeBotNext/BotOrchestrator.cs | 8 +- BridgeBotNext/BridgeBotNext.csproj | 2 +- BridgeBotNext/Program.cs | 2 +- BridgeBotNext/Providers/Tg/TgProvider.cs | 208 +++++++++++++++-------- BridgeBotNext/Providers/Vk/VkProvider.cs | 2 +- Dockerfile | 9 +- README.md | 27 ++- compose.dev.yaml | 10 ++ compose.yaml | 15 ++ 12 files changed, 249 insertions(+), 105 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 compose.dev.yaml create mode 100644 compose.yaml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..167247e --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,58 @@ +name: Create and publish a Docker images + +on: + push: + workflow_dispatch: + pull_request: + branches: [ "main" ] + +env: + REGISTRY: ghcr.io + REGISTRY_USERNAME: ${{ github.actor }} + REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + PLATFORMS: linux/amd64 + + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ env.REGISTRY_PASSWORD }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix={{branch}}-,enable=${{ startsWith(github.ref, 'refs/heads/') }} + type=ref,event=branch + type=ref,event=tag,enable=${{ startsWith(github.ref, 'refs/tags/') }} + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5.1.0 + with: + context: . + provenance: false + platforms: ${{ env.PLATFORMS }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index baa3b7e..1f5384b 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -15,10 +15,5 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.0.100 - - - name: Build with dotnet - run: dotnet build --configuration Release + - name: Build with docker + run: docker image build . \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index addfabd..2176a43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,9 +17,9 @@ jobs: run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 3.0.100 + dotnet-version: 3.0 - name: Build with dotnet (linux-x64) run: dotnet publish -c "Release" -r linux-x64 --self-contained true BridgeBotNext diff --git a/BridgeBotNext/BotOrchestrator.cs b/BridgeBotNext/BotOrchestrator.cs index b1eefe6..3cb4602 100644 --- a/BridgeBotNext/BotOrchestrator.cs +++ b/BridgeBotNext/BotOrchestrator.cs @@ -160,10 +160,10 @@ await e.Message.OriginConversation.SendMessage( 3) Введите полученную команду в другой беседе, где находится этот бот. Вы можете посмотреть текущие соединения с помощью команды /list -/Поддерживаемые_мессенджеры: {string.Join(", ", _providers.Select(p => p.DisplayName))} -/Версия_бота: {Program.Version} -/Страница_проекта: https://github.com/maksimkurb/BridgeBotNext -/Автор: max@cubly.ru, 2018-2020"); +- Поддерживаемые мессенджеры: {string.Join(", ", _providers.Select(p => p.DisplayName))} +- Версия бота: {Program.Version} +- Страница проекта: https://github.com/maksimkurb/BridgeBotNext +- Автор: max@cubly.ru, 2018-2024"); } private async Task OnDisconnectCommand(Provider.MessageEventArgs e, string command, string args) diff --git a/BridgeBotNext/BridgeBotNext.csproj b/BridgeBotNext/BridgeBotNext.csproj index 8696a48..8b33501 100644 --- a/BridgeBotNext/BridgeBotNext.csproj +++ b/BridgeBotNext/BridgeBotNext.csproj @@ -30,7 +30,7 @@ - + diff --git a/BridgeBotNext/Program.cs b/BridgeBotNext/Program.cs index e256f12..763d9aa 100644 --- a/BridgeBotNext/Program.cs +++ b/BridgeBotNext/Program.cs @@ -17,7 +17,7 @@ namespace BridgeBotNext { internal class Program { - public static string Version => "2.0.0"; + public static string Version => "2.1.0"; public static IWebHostBuilder CreateWebHostBuilder(string[] args) { diff --git a/BridgeBotNext/Providers/Tg/TgProvider.cs b/BridgeBotNext/Providers/Tg/TgProvider.cs index 75bba48..cba6a3b 100644 --- a/BridgeBotNext/Providers/Tg/TgProvider.cs +++ b/BridgeBotNext/Providers/Tg/TgProvider.cs @@ -92,7 +92,14 @@ public override async Task SendMessage(Conversation conversation, Message messag var body = FormatMessageBody(message, fwd); if (body.Length > 0) - await BotClient.SendTextMessageAsync(new ChatId(conversation.OriginId), $"{sender}{body}", ParseMode.Html, true); + { + await BotClient.SendTextMessageAsync( + chatId: chat, + text: $"{sender}{body}", + parseMode: ParseMode.Html, + disableWebPagePreview: true + ); + } #endregion @@ -113,8 +120,8 @@ Func AlbumAttachmentSelector() case PhotoAttachment albumPhoto: { if (albumPhoto.Meta is PhotoSize photo) - return new InputMediaPhoto(new InputMedia(photo.FileId)); - var tgPhoto = new InputMediaPhoto(new InputMedia(albumPhoto.Url)) + return new InputMediaPhoto(photo.FileId); + var tgPhoto = new InputMediaPhoto(albumPhoto.Url) { Caption = message?.OriginSender?.DisplayName != null ? $"[{message.OriginSender.DisplayName}] {albumPhoto.Caption ?? ""}" @@ -126,13 +133,13 @@ Func AlbumAttachmentSelector() case VideoAttachment albumVideo: { if (albumVideo.Meta is Video video) - return new InputMediaPhoto(new InputMedia(video.FileId)); + return new InputMediaVideo(video.FileId); var tgVideo = new InputMediaVideo(albumVideo.Url) { Caption = message?.OriginSender?.DisplayName != null ? $"[{message.OriginSender.DisplayName}] {albumVideo.Caption ?? ""}" : albumVideo.Caption, - Duration = (int) (albumVideo.Duration ?? 0), + Duration = (int)(albumVideo.Duration ?? 0), Width = albumVideo.Width, Height = albumVideo.Height }; @@ -150,87 +157,135 @@ Func AlbumAttachmentSelector() .GroupBy(tuple => tuple.i / 10); foreach (var chunk in chunks) + { + var media = chunk.Select(x => x.val).Select(AlbumAttachmentSelector()); await BotClient.SendMediaGroupAsync( - chunk.Select(x => x.val).Select(AlbumAttachmentSelector()), chat); + chatId: chat, + media: media + ); + } var restAttachments = attachments.Where(at => !(at is ITgGroupableAttachment)); foreach (var at in restAttachments) + { if (at is AnimationAttachment animation) { - await BotClient.SendAnimationAsync(chat, _getInputFile(message, animation), - (int) (animation.Duration ?? 0), - animation.Width, - animation.Height, caption: animation.Caption); + await BotClient.SendAnimationAsync( + chatId: chat, + animation: _getInputFile(message, animation), + duration: animation.Duration.HasValue ? (int)animation.Duration.Value : 0, + width: animation.Width, + height: animation.Height, + caption: animation.Caption + ); } else if (at is VoiceAttachment voice) { - var req = (HttpWebRequest) WebRequest.Create(voice.Url); + var req = (HttpWebRequest)WebRequest.Create(voice.Url); req.Timeout = 15000; - var resp = (HttpWebResponse) req.GetResponse(); - await BotClient.SendVoiceAsync(chat, - new InputOnlineFile(resp.GetResponseStream(), voice.FileName), voice.Caption); + var resp = (HttpWebResponse)req.GetResponse(); + await BotClient.SendVoiceAsync( + chatId: chat, + voice: new InputOnlineFile(resp.GetResponseStream(), voice.FileName), + caption: voice.Caption + ); } else if (at is AudioAttachment audio) { - await BotClient.SendAudioAsync(chat, _getInputFile(message, audio), audio.Caption, - duration: (int) (audio.Duration ?? 0), - performer: audio.Performer, title: audio.Title); + await BotClient.SendAudioAsync( + chatId: chat, + audio: _getInputFile(message, audio), + caption: audio.Caption, + duration: audio.Duration.HasValue ? (int)audio.Duration.Value : 0, + performer: audio.Performer, + title: audio.Title + ); } else if (at is ContactAttachment contact) { - await BotClient.SendContactAsync(chat, contact.Phone, contact.FirstName, contact.LastName, - vCard: contact.VCard); + await BotClient.SendContactAsync( + chatId: chat, + phoneNumber: contact.Phone, + firstName: contact.FirstName, + lastName: contact.LastName, + vCard: contact.VCard + ); } else if (at is LinkAttachment link) { - await BotClient.SendTextMessageAsync(chat, $"{sender}{link.Url}", ParseMode.Html); + await BotClient.SendTextMessageAsync( + chatId: chat, + text: $"{sender}{link.Url}", + parseMode: ParseMode.Html + ); } else if (at is StickerAttachment sticker) { var inputFile = _getInputFile(message, sticker); if (sticker.MimeType == "image/webp") { - await BotClient.SendStickerAsync(chat, inputFile); + await BotClient.SendStickerAsync( + chatId: chat, + sticker: inputFile + ); } else { Logger.LogTrace("Converting sticker to webp format"); - var req = (HttpWebRequest) WebRequest.Create(inputFile.Url); + var req = (HttpWebRequest)WebRequest.Create(inputFile.Url); req.Timeout = 15000; - var resp = (HttpWebResponse) req.GetResponse(); + var resp = (HttpWebResponse)req.GetResponse(); var image = SKImage.FromBitmap(SKBitmap.Decode(resp.GetResponseStream())); using (var p = image.Encode(SKEncodedImageFormat.Webp, 100)) { - await BotClient.SendStickerAsync(chat, - new InputMedia(p.AsStream(), "sticker.webp")); + await BotClient.SendStickerAsync( + chatId: chat, + sticker: new InputOnlineFile(p.AsStream(), "sticker.webp") + ); } } } else if (at is PlaceAttachment place) { if (place.Name != null && place.Address != null) - await BotClient.SendVenueAsync(chat, (float) place.Latitude, (float) place.Longitude, - place.Name, - place.Address); + await BotClient.SendVenueAsync( + chatId: chat, + latitude: (float)place.Latitude, + longitude: (float)place.Longitude, + title: place.Name, + address: place.Address + ); else - await BotClient.SendLocationAsync(chat, (float) place.Latitude, (float) place.Longitude); + await BotClient.SendLocationAsync( + chatId: chat, + latitude: (float)place.Latitude, + longitude: (float)place.Longitude + ); } else if (at is FileAttachment file) { if (file.MimeType == "image/gif" || file.MimeType == "application/pdf" || file.MimeType == "application/zip") { - await BotClient.SendDocumentAsync(chat, _getInputFile(message, file), file.Caption); + await BotClient.SendDocumentAsync( + chatId: chat, + document: _getInputFile(message, file), + caption: file.Caption + ); } else { - var req = (HttpWebRequest) WebRequest.Create(file.Url); + var req = (HttpWebRequest)WebRequest.Create(file.Url); req.Timeout = 15000; - var resp = (HttpWebResponse) req.GetResponse(); - await BotClient.SendDocumentAsync(chat, - new InputOnlineFile(resp.GetResponseStream(), file.FileName), file.Caption); + var resp = (HttpWebResponse)req.GetResponse(); + await BotClient.SendDocumentAsync( + chatId: chat, + document: new InputOnlineFile(resp.GetResponseStream(), file.FileName), + caption: file.Caption + ); } } + } } #endregion @@ -241,21 +296,21 @@ private InputOnlineFile _getInputFile(Message message, Attachment attachment) if (message.OriginSender != null && message.OriginSender.Provider.Equals(this)) { if (attachment.Meta is Audio audio) - return audio.FileId; + return new InputOnlineFile(audio.FileId); if (attachment.Meta is Document document) - return document.FileId; + return new InputOnlineFile(document.FileId); if (attachment.Meta is Animation animation) - return animation.FileId; + return new InputOnlineFile(animation.FileId); if (attachment.Meta is PhotoSize photo) - return photo.FileId; + return new InputOnlineFile(photo.FileId); if (attachment.Meta is Sticker sticker) - return sticker.FileId; + return new InputOnlineFile(sticker.FileId); if (attachment.Meta is Video video) - return video.FileId; + return new InputOnlineFile(video.FileId); if (attachment.Meta is Voice voice) - return voice.FileId; + return new InputOnlineFile(voice.FileId); if (attachment.Meta is VideoNote videoNote) - return videoNote.FileId; + return new InputOnlineFile(videoNote.FileId); } return new InputOnlineFile(attachment.Url); @@ -271,7 +326,7 @@ private TgPerson _extractPerson(User tgUser) var fullName = new StringBuilder().Append(tgUser.FirstName); if (tgUser.LastName != null) fullName.AppendFormat(" {0}", tgUser.LastName); - return new TgPerson(this, tgUser.Id, tgUser.Username, fullName.ToString()); + return new TgPerson(this, Convert.ToInt32(tgUser.Id), tgUser.Username, fullName.ToString()); } /** @@ -380,15 +435,15 @@ private async Task _extractMessage(Telegram.Bot.Types.Message tgMessage if (tgMessage.Entities != null) attachments.AddRange(from entity in tgMessage.Entities - where entity.Type == MessageEntityType.TextLink - select new LinkAttachment(entity.Url)); + where entity.Type == MessageEntityType.TextLink + select new LinkAttachment(entity.Url)); // Just forwarded message if (tgMessage.ForwardFrom != null) { var fwdPerson = _extractPerson(tgMessage.ForwardFrom); var fwdMessage = new Message(conversation, fwdPerson, tgMessage.Text, attachments: attachments); - return new Message(conversation, person, forwardedMessages: new[] {fwdMessage}); + return new Message(conversation, person, forwardedMessages: new[] { fwdMessage }); } // Reply to @@ -411,43 +466,50 @@ await _extractMessage(tgMessage.ReplyToMessage) private async void _onMessage(object sender, Telegram.Bot.Args.MessageEventArgs e) { - if (e.Message == null) - { - return; - } - - if ( - e.Message.Text != null - && e.Message.Text.StartsWith("/") - && !e.Message.Text.Contains(" ") - && e.Message.Text.Contains("@") - ) + try { - var botName = $"@{BotUserName}"; - if (!e.Message.Text.EndsWith(botName)) + if (e.Message == null) { - Logger.LogDebug("Message is a command that targets another bot. Skipping it..."); return; } - e.Message.Text = e.Message.Text.Substring(0, e.Message.Text.Length - botName.Length); - } + if ( + e.Message.Text != null + && e.Message.Text.StartsWith("/") + && !e.Message.Text.Contains(" ") + && e.Message.Text.Contains("@") + ) + { + var botName = $"@{BotUserName}"; + if (!e.Message.Text.EndsWith(botName)) + { + Logger.LogDebug("Message is a command that targets another bot. Skipping it..."); + return; + } - var message = await _extractMessage(e.Message); + e.Message.Text = e.Message.Text.Substring(0, e.Message.Text.Length - botName.Length); + } - if (e.Message.MediaGroupId != null && message.Attachments.Any()) - { - _mediaGroupSettler.AddMediaGroupMessage(e.Message.MediaGroupId, message, message.Attachments.First()); - return; - } + var message = await _extractMessage(e.Message); - if (!string.IsNullOrEmpty(message.Body) && message.Body.StartsWith("/")) + if (e.Message.MediaGroupId != null && message.Attachments.Any()) + { + _mediaGroupSettler.AddMediaGroupMessage(e.Message.MediaGroupId, message, message.Attachments.First()); + return; + } + + if (!string.IsNullOrEmpty(message.Body) && message.Body.StartsWith("/")) + { + OnCommandReceived(new MessageEventArgs(message)); + return; + } + + OnMessageReceived(new MessageEventArgs(message)); + } + catch (Exception ex) { - OnCommandReceived(new MessageEventArgs(message)); - return; + Logger.LogError(ex, "Error processing incoming message"); } - - OnMessageReceived(new MessageEventArgs(message)); } private void _onMediaGroupMessage(object sender, TgMediaGroupSettler.MediaGroupEventArgs e) diff --git a/BridgeBotNext/Providers/Vk/VkProvider.cs b/BridgeBotNext/Providers/Vk/VkProvider.cs index 580242c..40c246a 100644 --- a/BridgeBotNext/Providers/Vk/VkProvider.cs +++ b/BridgeBotNext/Providers/Vk/VkProvider.cs @@ -67,7 +67,7 @@ public VkProvider(ILogger logger, } public override string Name => "vk"; - public override string DisplayName => "Vkontakte"; + public override string DisplayName => "ВКонтакте"; private void _setApiVersion(int major, int minor) { diff --git a/Dockerfile b/Dockerfile index 660cc4d..27659e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,14 +19,7 @@ ENTRYPOINT ["dotnet", "BridgeBotNext.dll"] RUN mkdir /data -#ENV BOT_VK__ACCESSTOKEN abc -#ENV BOT_VK__GROUPID -#ENV BOT_TG__BOTTOKEN -ENV BOT_AUTH__ENABLED false -#ENV BOT_AUTH__PASSWORD - -ENV BOT_DBPROVIDER sqlite -ENV ConnectionStrings__sqlite "Data Source=/data/bridgebotnext.db" +ENV ConnectionStrings__sqlite="Data Source=/data/bridgebotnext.db" VOLUME /data diff --git a/README.md b/README.md index 6d0eff0..bb29704 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # BridgeBotNext -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/maksimkurb/BridgeBotNext)](https://github.com/maksimkurb/BridgeBotNext/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/maksimkurb/bridge-bot-next)](https://hub.docker.com/r/maksimkurb/bridge-bot-next) [![build](https://github.com/maksimkurb/BridgeBotNext/workflows/build/badge.svg)](https://github.com/maksimkurb/BridgeBotNext/actions?query=workflow%3Abuild) [![release](https://github.com/maksimkurb/BridgeBotNext/workflows/release/badge.svg)](https://github.com/maksimkurb/BridgeBotNext/actions?query=workflow%3Arelease) [![GitHub stars](https://img.shields.io/github/stars/maksimkurb/BridgeBotNext?style=social)](https://github.com/maksimkurb/BridgeBotNext/stargazers) + +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/maksimkurb/BridgeBotNext)](https://github.com/maksimkurb/BridgeBotNext/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/maksimkurb/bridge-bot-next)](https://hub.docker.com/r/maksimkurb/bridge-bot-next) [![build](https://github.com/maksimkurb/BridgeBotNext/workflows/build/badge.svg)](https://github.com/maksimkurb/BridgeBotNext/actions?query=workflow%3Abuild) [![release](https://github.com/maksimkurb/BridgeBotNext/workflows/release/badge.svg)](https://github.com/maksimkurb/BridgeBotNext/actions?query=workflow%3Arelease) [![GitHub stars](https://img.shields.io/github/stars/maksimkurb/BridgeBotNext?style=social)](https://github.com/maksimkurb/BridgeBotNext/stargazers)

@@ -15,29 +16,36 @@ To connect chats, enter `/token` command in first chat to get a special command Enter this special command in another chat (it looks like `/connect $mbb2$1!9d8xxxxx00ca`) and your chats are connected now! ## Screenshot + ![Screenshot](https://raw.githubusercontent.com/maksimkurb/BridgeBotNext/master/static/screenshot.jpg) ## Deployment -### Docker +### Docker Compose You can run bot by creating docker container: + ```bash -docker run --name bridge-bot \ - -e BOT_VK__ACCESSTOKEN=abcdef \ - -e BOT_VK__GROUPID=123456 \ - -e BOT_TG__BOTTOKEN="1235467:abcdefg" \ - -v ./bridge-bot-data:/data \ - maksimkurb/bridge-bot-next:1.0 +# download compose file +wget https://github.com/maksimkurb/BridgeBotNext/raw/refs/heads/master/compose.yaml + +# then edit env values in compose.yaml file +nano compose.yaml + +# and deploy +docker compose up -d ``` ### Heroku + You can deploy bot to Heroku with 1-click button: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/maksimkurb/BridgeBotNext) ### Manual + For Linux, make sure you have insalled libfontconfig: + ```bash apt-get install -y libfontconfig1 ``` @@ -47,6 +55,7 @@ Then, just download latest release, create appsettings.json, configure it and ru ## Configuration ### Environment + You can configure bot via the following environment variables: |Key (notice double underscore) |Sample value | Description | @@ -61,7 +70,9 @@ You can configure bot via the following environment variables: | BOT_CONNECTIONSTRINGS_POSTGRES | Host=localhost;Database=postgres;Username=postgres;Password=postgres | Connection string for postgres | ### appsettings.json file + You can create `appsettings.json` configuration file, place it in the folder with BridgeBotNext: + ```json { "Vk": { diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..1cf4a08 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,10 @@ +name: bridgebot-next +services: + bridgebot-next: + extends: + file: compose.yaml + service: bridgebot-next + build: + context: ./ + dockerfile: Dockerfile + diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4655f63 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +name: bridgebot-next +services: + bridgebot-next: + container_name: bridgebot-next + volumes: + - ./docker-data:/data + image: ghcr.io/maksimkurb/bridge-bot-next + environment: + BOT_VK__ACCESSTOKEN: "vk1.a...." + BOT_VK__GROUPID: 12345678 + BOT_TG__BOTTOKEN: "123455678:AAbcdefg..." + BOT_AUTH__ENABLED: "true" + BOT_AUTH__PASSWORD: "..." + BOT_DBPROVIDER: "sqlite" + BOT_CONNECTIONSTRINGS_SQLITE: "Data Source=./database.db"