mirror of
https://github.com/yawaflua/Aoyo.Taiga.git
synced 2025-12-08 19:39:28 +02:00
Create project file
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
61
.github/workflows/main.yml
vendored
Normal file
61
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: .NET CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master", "develop", "main" ]
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- '*.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Unit and Integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --verbosity normal
|
||||||
|
|
||||||
|
compose:
|
||||||
|
name: Push Docker image to ghcr.io
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Aoyo.Taiga/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
29
.github/workflows/nonmain.yml
vendored
Normal file
29
.github/workflows/nonmain.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: .NET CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- '*.md'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Unit and Integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --verbosity normal
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
22
Aoyo.Taiga.sln
Normal file
22
Aoyo.Taiga.sln
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aoyo.Taiga", "Aoyo.Taiga\Aoyo.Taiga.csproj", "{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B8D94470-53F6-4C05-BADF-1A97910B3B74}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
compose.yaml = compose.yaml
|
||||||
|
README.md = README.md
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
22
Aoyo.Taiga/Aoyo.Taiga.csproj
Normal file
22
Aoyo.Taiga/Aoyo.Taiga.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net" Version="3.18.0" />
|
||||||
|
<PackageReference Include="Discord.Net.WebSocket" Version="3.18.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
Aoyo.Taiga/Aoyo.Taiga.http
Normal file
6
Aoyo.Taiga/Aoyo.Taiga.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@Aoyo.Taiga_HostAddress = http://localhost:8080
|
||||||
|
|
||||||
|
GET {{Aoyo.Taiga_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
446
Aoyo.Taiga/Controllers/TaigaWebHook.cs
Normal file
446
Aoyo.Taiga/Controllers/TaigaWebHook.cs
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Aoyo.Taiga.WebHookTypes;
|
||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.Controllers;
|
||||||
|
|
||||||
|
|
||||||
|
[Route("api/v1/TaigaWebHook")]
|
||||||
|
[ApiController]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedVariable")]
|
||||||
|
[SuppressMessage("ReSharper", "UnusedMember.Local")]
|
||||||
|
public class TaigaWebHook : Controller
|
||||||
|
{
|
||||||
|
private const string ChannelIdConfigurationPath = "Discord:Id";
|
||||||
|
private const string TaigaKeyPath = "Taiga:Key";
|
||||||
|
private readonly ulong _id;
|
||||||
|
private ITextChannel? _logChannel;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly ILogger<TaigaWebHook> _logger;
|
||||||
|
private readonly string _key;
|
||||||
|
private IConfiguration _config;
|
||||||
|
|
||||||
|
public TaigaWebHook(DiscordSocketClient client, ILogger<TaigaWebHook> logger, IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_client = client;
|
||||||
|
_logger = logger;
|
||||||
|
_id = config.GetValue<ulong?>(ChannelIdConfigurationPath) ?? throw new NullReferenceException("Channel id should to be provided");
|
||||||
|
_key = config.GetValue<string>(TaigaKeyPath) ?? throw new NullReferenceException("Taiga KEY should to be provided!") ;
|
||||||
|
_logChannel = client.GetChannel(_id) as ITextChannel ?? throw new NullReferenceException("Channel not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("post")]
|
||||||
|
public async Task<IActionResult> PostAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var signature = Request.Headers["X-TAIGA-WEBHOOK-SIGNATURE"].First()!;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(Request.Body);
|
||||||
|
var data = await reader.ReadToEndAsync();
|
||||||
|
_logger.LogInformation(data);
|
||||||
|
|
||||||
|
if (_config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Production")
|
||||||
|
if (!VerifySignature(_key, data, signature))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var webhookType = DetermineWebhookType(data);
|
||||||
|
|
||||||
|
switch (webhookType)
|
||||||
|
{
|
||||||
|
case "userstory":
|
||||||
|
await HandleUserStoryWebhook(data);
|
||||||
|
break;
|
||||||
|
case "issue":
|
||||||
|
await HandleIssueWebhook(data);
|
||||||
|
break;
|
||||||
|
case "task":
|
||||||
|
await HandleTaskWebhook(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.LogWarning("Unsupported type of webhook: {WebhookType}", webhookType);
|
||||||
|
return BadRequest($"Unsupported type of webhook: {webhookType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError(exception, exception.Message);
|
||||||
|
return BadRequest(exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifySignature(string key, string data, string signature)
|
||||||
|
{
|
||||||
|
using HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
|
||||||
|
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||||
|
var computedHash = Convert.ToHexString(hashBytes).ToLower();
|
||||||
|
|
||||||
|
return computedHash.Length == signature.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleUserStoryWebhook(string json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webhook = TaigaWebhookParser.ParseUserStoryWebhook(json);
|
||||||
|
var userStory = webhook.Data;
|
||||||
|
var action = webhook.GetAction();
|
||||||
|
|
||||||
|
_logger.LogInformation("Handling User Story: {Subject} (ID: {Id}), Action: {Action}",
|
||||||
|
userStory.Subject, userStory.Id, action);
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case WebhookAction.Create:
|
||||||
|
await OnUserStoryCreated(userStory, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Change:
|
||||||
|
await OnUserStoryChanged(userStory, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Delete:
|
||||||
|
await OnUserStoryDeleted(userStory, webhook);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error when handled User Story webhook");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleIssueWebhook(string json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webhook = TaigaWebhookParser.ParseIssueWebhook(json);
|
||||||
|
var issue = webhook.Data;
|
||||||
|
var action = webhook.GetAction();
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing Issue: {Subject} (ID: {Id}), Action: {Action}",
|
||||||
|
issue.Subject, issue.Id, action);
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case WebhookAction.Create:
|
||||||
|
await OnIssueCreated(issue, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Change:
|
||||||
|
await OnIssueChanged(issue, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Delete:
|
||||||
|
await OnIssueDeleted(issue, webhook);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error when processing Issue webhook");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleTaskWebhook(string json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webhook = TaigaWebhookParser.ParseTaskWebhook(json);
|
||||||
|
var task = webhook.Data;
|
||||||
|
var action = webhook.GetAction();
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing Task: {Subject} (ID: {Id}), Action: {Action}",
|
||||||
|
task.Subject, task.Id, action);
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case WebhookAction.Create:
|
||||||
|
await OnTaskCreated(task, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Change:
|
||||||
|
await OnTaskChanged(task, webhook);
|
||||||
|
break;
|
||||||
|
case WebhookAction.Delete:
|
||||||
|
await OnTaskDeleted(task, webhook);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error when processing Task webhook");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUserStoryCreated(TaigaUserStory userStory, UserStoryWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Created new User Story: {Subject} in project {ProjectName}",
|
||||||
|
userStory.Subject, userStory.Project.Name);
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Created",
|
||||||
|
Type = "User Story",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{userStory.Subject}**\n" +
|
||||||
|
$"> Project: {userStory.Project.Name}\n" +
|
||||||
|
$"> Status: {userStory.Status.Name}\n" +
|
||||||
|
$"> Points: {userStory.TotalPoints}",
|
||||||
|
Color = Color.Green
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUserStoryChanged(TaigaUserStory userStory, UserStoryWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Changed User Story: {Subject}", userStory.Subject);
|
||||||
|
|
||||||
|
var changes = "";
|
||||||
|
if (webhook.Change?.Diff != null)
|
||||||
|
{
|
||||||
|
foreach (var change in webhook.Change.Diff)
|
||||||
|
{
|
||||||
|
changes += $"• {change.Key}: {change.Value.From} → {change.Value.To}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Changed",
|
||||||
|
Type = "User Story",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{userStory.Subject}**\n" +
|
||||||
|
$"> Project: {userStory.Project.Name}\n" +
|
||||||
|
$"> Changes:\n{changes}",
|
||||||
|
Color = Color.LightOrange
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUserStoryDeleted(TaigaUserStory userStory, UserStoryWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleted User Story: {Subject} (ID: {Id})",
|
||||||
|
userStory.Subject, userStory.Id);
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Deleted",
|
||||||
|
Type = "User Story",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{userStory.Subject}**\n" +
|
||||||
|
$"> Project: {userStory.Project.Name}\n" +
|
||||||
|
$"> ID: {userStory.Id}",
|
||||||
|
Color = Color.DarkRed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Методы для обработки Issue событий
|
||||||
|
private async Task OnIssueCreated(TaigaIssue issue, IssueWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Created new Issue: {Subject} in project {ProjectName}",
|
||||||
|
issue.Subject, issue.Project.Name);
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Created",
|
||||||
|
Type = "Issue",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{issue.Subject}**\n" +
|
||||||
|
$"> Project: {issue.Project.Name}\n" +
|
||||||
|
$"> Type: {issue.Type.Name}\n" +
|
||||||
|
$"> Prority: {issue.Priority.Name}",
|
||||||
|
Color = Color.Blue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnIssueChanged(TaigaIssue issue, IssueWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Changed Issue: {Subject}", issue.Subject);
|
||||||
|
|
||||||
|
var changes = "";
|
||||||
|
if (webhook.Change?.Diff != null)
|
||||||
|
{
|
||||||
|
foreach (var change in webhook.Change.Diff)
|
||||||
|
{
|
||||||
|
changes += $"> • {change.Key}: {change.Value.From} → {change.Value.To}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Changed",
|
||||||
|
Type = "Issue",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{issue.Subject}**\n" +
|
||||||
|
$"> Project: {issue.Project.Name}\n" +
|
||||||
|
$"> Changes:\n{changes}",
|
||||||
|
Color = Color.LightOrange
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnIssueDeleted(TaigaIssue issue, IssueWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleted Issue: {Subject} (ID: {Id})",
|
||||||
|
issue.Subject, issue.Id);
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Deleted",
|
||||||
|
Type = "Issue",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{issue.Subject}**\n" +
|
||||||
|
$"> Project: {issue.Project.Name}\n" +
|
||||||
|
$"> ID: {issue.Id}",
|
||||||
|
Color = Color.Red
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTaskCreated(TaigaTask task, TaskWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Created new Task: {Subject} in project {ProjectName}",
|
||||||
|
task.Subject, task.Project.Name);
|
||||||
|
|
||||||
|
var description = $"**{task.Subject}**\n" +
|
||||||
|
$"> Project: {task.Project.Name}\n" +
|
||||||
|
$"> Исполнитель: {task.AssignedTo?.FullName ?? "Не назначен"}";
|
||||||
|
|
||||||
|
if (task.UserStory != null)
|
||||||
|
{
|
||||||
|
description += $"\n> User Story: {task.UserStory.Subject} (#{task.UserStory.Ref})";
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Created",
|
||||||
|
Type = "Task",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = description,
|
||||||
|
Color = Color.Green
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTaskChanged(TaigaTask task, TaskWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Changed Task: {Subject}", task.Subject);
|
||||||
|
|
||||||
|
var changes = "";
|
||||||
|
if (webhook.Change?.Diff != null)
|
||||||
|
{
|
||||||
|
foreach (var change in webhook.Change.Diff)
|
||||||
|
{
|
||||||
|
changes += $"• {change.Key}: {change.Value.From} → {change.Value.To}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Changed",
|
||||||
|
Type = "Task",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{task.Subject}**\n" +
|
||||||
|
$"> Project: {task.Project.Name}\n" +
|
||||||
|
$"> Changes:\n{changes}",
|
||||||
|
Color = Color.Orange
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTaskDeleted(TaigaTask task, TaskWebhookPayload? webhook)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleted Task: {Subject} (ID: {Id})",
|
||||||
|
task.Subject, task.Id);
|
||||||
|
|
||||||
|
await SendDiscordNotification(new DiscordDTO
|
||||||
|
{
|
||||||
|
By = webhook.By,
|
||||||
|
Action = "Deleted",
|
||||||
|
Type = "Task",
|
||||||
|
Date = webhook.Date,
|
||||||
|
Description = $"**{task.Subject}**\n" +
|
||||||
|
$"> Project: {task.Project.Name}\n" +
|
||||||
|
$"> ID: {task.Id}",
|
||||||
|
Color = Color.Red
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendDiscordNotification(DiscordDTO payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logChannel ??= _client.GetChannel(_id) as ITextChannel;
|
||||||
|
|
||||||
|
if (_logChannel == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Не удалось получить Discord канал с ID: {ChannelId}", _id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _logChannel.SendMessageAsync(embed:
|
||||||
|
new EmbedBuilder()
|
||||||
|
.WithAuthor(payload.By.Username, payload.By.Photo)
|
||||||
|
.WithTitle($"{payload.Action} {payload.Type.ToLower()}")
|
||||||
|
.WithDescription(payload.Description)
|
||||||
|
.WithColor(payload.Color)
|
||||||
|
.WithTimestamp(payload.Date)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error when sending message into Discord");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string DetermineWebhookType(string json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
var root = document.RootElement;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("type", out var typeElement))
|
||||||
|
{
|
||||||
|
return typeElement.GetString()?.ToLower()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("data", out var dataElement))
|
||||||
|
{
|
||||||
|
if (dataElement.TryGetProperty("points", out _))
|
||||||
|
{
|
||||||
|
return "userstory";
|
||||||
|
}
|
||||||
|
else if (dataElement.TryGetProperty("type", out _) && dataElement.TryGetProperty("priority", out _))
|
||||||
|
{
|
||||||
|
return "issue";
|
||||||
|
}
|
||||||
|
else if (dataElement.TryGetProperty("user_story", out _))
|
||||||
|
{
|
||||||
|
return "task";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Cannot parse this type of webhook");
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
Aoyo.Taiga/Discord/DiscordHandler.cs
Normal file
51
Aoyo.Taiga/Discord/DiscordHandler.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.Discord;
|
||||||
|
|
||||||
|
public class DiscordHandler(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
InteractionService interactionService,
|
||||||
|
ILogger<DiscordHandler> logger,
|
||||||
|
IServiceScope provider)
|
||||||
|
{
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
client.Ready += ClientOnReady;
|
||||||
|
client.Log += ClientOnLog;
|
||||||
|
await interactionService.AddModulesAsync(Assembly.GetEntryAssembly(), provider.ServiceProvider);
|
||||||
|
client.InteractionCreated += ClientOnInteractionCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClientOnInteractionCreated(SocketInteraction arg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = new SocketInteractionContext(client, arg);
|
||||||
|
var result = await interactionService.ExecuteCommandAsync(context, provider.ServiceProvider);
|
||||||
|
if (result.Error is not null)
|
||||||
|
{
|
||||||
|
throw new Exception($"{result.ErrorReason}: {result.Error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await arg.RespondAsync(":x: " + ex.Message + "", ephemeral: true);
|
||||||
|
logger.LogError(ex, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ClientOnLog(LogMessage arg)
|
||||||
|
{
|
||||||
|
logger.LogInformation(exception: arg.Exception, message:arg.Message);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClientOnReady()
|
||||||
|
{
|
||||||
|
await interactionService.RegisterCommandsGloballyAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Aoyo.Taiga/Discord/DiscordService.cs
Normal file
27
Aoyo.Taiga/Discord/DiscordService.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.Discord;
|
||||||
|
|
||||||
|
public class DiscordService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<DiscordService> logger,
|
||||||
|
DiscordHandler interactionHandler)
|
||||||
|
: BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
client.Log += ClientOnLog;
|
||||||
|
await client.LoginAsync(TokenType.Bot, configuration.GetValue<string>("Discord:Token"));
|
||||||
|
await client.StartAsync();
|
||||||
|
await interactionHandler.InitializeAsync();
|
||||||
|
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ClientOnLog(LogMessage arg)
|
||||||
|
{
|
||||||
|
logger.LogInformation(exception: arg.Exception, message:arg.Message);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Aoyo.Taiga/Dockerfile
Normal file
39
Aoyo.Taiga/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["Aoyo.Taiga/Aoyo.Taiga.csproj", "Aoyo.Taiga/"]
|
||||||
|
RUN dotnet restore "Aoyo.Taiga/Aoyo.Taiga.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/Aoyo.Taiga"
|
||||||
|
RUN dotnet build "./Aoyo.Taiga.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./Aoyo.Taiga.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y curl && \
|
||||||
|
rm -rf /var/lib/apt/lists/* \
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/aoyo/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "Aoyo.Taiga.dll"]
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Aoyo.Taiga" \
|
||||||
|
org.opencontainers.image.description="Taiga`s webhook catcher" \
|
||||||
|
org.opencontainers.image.version="1.0.0" \
|
||||||
|
org.opencontainers.image.url="https://github.com/yawaflua/Aoyo.Taiga" \
|
||||||
|
org.opencontainers.image.source="https://github.com/yawaflua/Aoyo.Taiga" \
|
||||||
|
org.opencontainers.image.licenses="Apache-2.0" \
|
||||||
|
org.opencontainers.image.authors="Dmitrii Shimanskii <yawaflua@outlook.co.il>" \
|
||||||
|
org.opencontainers.image.created="2025-07-29T12:00:00Z" \
|
||||||
|
org.opencontainers.image.vendor="Diamond Studio"
|
||||||
18
Aoyo.Taiga/Program.cs
Normal file
18
Aoyo.Taiga/Program.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.AspNetCore;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
WebHost.
|
||||||
|
CreateDefaultBuilder(args).
|
||||||
|
UseStartup<Startup>().
|
||||||
|
UseKestrel(
|
||||||
|
l =>
|
||||||
|
l.ListenAnyIP(8080)
|
||||||
|
).
|
||||||
|
Build().Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Aoyo.Taiga/Properties/launchSettings.json
Normal file
23
Aoyo.Taiga/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:8080",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http_release": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:8080",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Aoyo.Taiga/Startup.cs
Normal file
53
Aoyo.Taiga/Startup.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Aoyo.Taiga.Discord;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga;
|
||||||
|
|
||||||
|
public class Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
private readonly DiscordSocketConfig _socketConfig = new()
|
||||||
|
{
|
||||||
|
GatewayIntents = GatewayIntents.All,
|
||||||
|
AlwaysDownloadUsers = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
var factory = LoggerFactory.Create(builder => builder.AddFilter(k => k >=
|
||||||
|
(configuration.GetValue<LogLevel?>("Logging:LogLevel:Default") ??
|
||||||
|
LogLevel.Information)).AddConsole());
|
||||||
|
services.AddControllers();
|
||||||
|
services.AddHttpLogging();
|
||||||
|
services.AddSingleton(factory);
|
||||||
|
services.AddRouting();
|
||||||
|
services.AddSingleton(_socketConfig);
|
||||||
|
services.AddSingleton(configuration);
|
||||||
|
services.AddSingleton<DiscordSocketClient>();
|
||||||
|
services.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()));
|
||||||
|
services.AddSingleton<DiscordHandler>();
|
||||||
|
services.AddSingleton(x => x.CreateScope());
|
||||||
|
services.AddHostedService<DiscordService>();
|
||||||
|
services.AddHealthChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UsePathBase("/aoyo/");
|
||||||
|
app.UseHealthChecks("/health");
|
||||||
|
app.UseHttpLogging();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints => {
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapHealthChecks("/heath");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Aoyo.Taiga/WebHookTypes/BaseTaigaItem.cs
Normal file
54
Aoyo.Taiga/WebHookTypes/BaseTaigaItem.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public abstract class BaseTaigaItem : ITaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ref")]
|
||||||
|
public int Ref { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("subject")]
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("created_date")]
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("modified_date")]
|
||||||
|
public DateTime? ModifiedDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("owner")]
|
||||||
|
public TaigaUser Owner { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("assigned_to")]
|
||||||
|
public TaigaUser? AssignedTo { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("project")]
|
||||||
|
public TaigaProject Project { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public TaigaStatus Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tags")]
|
||||||
|
public List<string> Tags { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[JsonPropertyName("is_closed")]
|
||||||
|
public bool IsClosed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_blocked")]
|
||||||
|
public bool IsBlocked { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("blocked_note")]
|
||||||
|
public string BlockedNote { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public int Version { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("watchers")]
|
||||||
|
public List<int> Watchers { get; set; } = new List<int>();
|
||||||
|
}
|
||||||
13
Aoyo.Taiga/WebHookTypes/DiscordDTO.cs
Normal file
13
Aoyo.Taiga/WebHookTypes/DiscordDTO.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class DiscordDTO
|
||||||
|
{
|
||||||
|
public TaigaUser By { get; set; }
|
||||||
|
public string Action { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public Color Color { get; set; }
|
||||||
|
}
|
||||||
55
Aoyo.Taiga/WebHookTypes/ITaigaItem.cs
Normal file
55
Aoyo.Taiga/WebHookTypes/ITaigaItem.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public interface ITaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ref")]
|
||||||
|
public int Ref { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("subject")]
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("created_date")]
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("modified_date")]
|
||||||
|
public DateTime? ModifiedDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("owner")]
|
||||||
|
public TaigaUser Owner { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("assigned_to")]
|
||||||
|
public TaigaUser AssignedTo { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("project")]
|
||||||
|
public TaigaProject Project { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public TaigaStatus Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tags")]
|
||||||
|
public List<string> Tags { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_closed")]
|
||||||
|
public bool IsClosed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_blocked")]
|
||||||
|
public bool IsBlocked { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("blocked_note")]
|
||||||
|
public string BlockedNote { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public int Version { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("watchers")]
|
||||||
|
public List<int> Watchers { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
5
Aoyo.Taiga/WebHookTypes/Payloads.cs
Normal file
5
Aoyo.Taiga/WebHookTypes/Payloads.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class UserStoryWebhookPayload : TaigaWebhookPayload<TaigaUserStory> { }
|
||||||
|
public class IssueWebhookPayload : TaigaWebhookPayload<TaigaIssue> { }
|
||||||
|
public class TaskWebhookPayload : TaigaWebhookPayload<TaigaTask> { }
|
||||||
15
Aoyo.Taiga/WebHookTypes/TaigaChange.cs
Normal file
15
Aoyo.Taiga/WebHookTypes/TaigaChange.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaChange
|
||||||
|
{
|
||||||
|
[JsonPropertyName("comment")]
|
||||||
|
public string Comment { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("comment_html")]
|
||||||
|
public string CommentHtml { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("diff")]
|
||||||
|
public Dictionary<string, TaigaFieldChange> Diff { get; set; } = new Dictionary<string, TaigaFieldChange>();
|
||||||
|
}
|
||||||
12
Aoyo.Taiga/WebHookTypes/TaigaFieldChange.cs
Normal file
12
Aoyo.Taiga/WebHookTypes/TaigaFieldChange.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaFieldChange
|
||||||
|
{
|
||||||
|
[JsonPropertyName("from")]
|
||||||
|
public object From { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("to")]
|
||||||
|
public object To { get; set; }
|
||||||
|
}
|
||||||
30
Aoyo.Taiga/WebHookTypes/TaigaIssue.cs
Normal file
30
Aoyo.Taiga/WebHookTypes/TaigaIssue.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaIssue : BaseTaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public TaigaType Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("priority")]
|
||||||
|
public TaigaPriority Priority { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("severity")]
|
||||||
|
public TaigaSeverity Severity { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("milestone")]
|
||||||
|
public TaigaMilestone Milestone { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("generated_user_stories")]
|
||||||
|
public List<TaigaUserStory> GeneratedUserStories { get; set; } = new List<TaigaUserStory>();
|
||||||
|
|
||||||
|
[JsonPropertyName("external_reference")]
|
||||||
|
public List<string> ExternalReference { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[JsonPropertyName("due_date")]
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("due_date_reason")]
|
||||||
|
public string DueDateReason { get; set; }
|
||||||
|
}
|
||||||
21
Aoyo.Taiga/WebHookTypes/TaigaMilestone.cs
Normal file
21
Aoyo.Taiga/WebHookTypes/TaigaMilestone.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaMilestone
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("slug")]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("estimated_start")]
|
||||||
|
public DateTime? EstimatedStart { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("estimated_finish")]
|
||||||
|
public DateTime? EstimatedFinish { get; set; }
|
||||||
|
}
|
||||||
13
Aoyo.Taiga/WebHookTypes/TaigaPoint.cs
Normal file
13
Aoyo.Taiga/WebHookTypes/TaigaPoint.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaPoint
|
||||||
|
{
|
||||||
|
[JsonPropertyName("role")]
|
||||||
|
public string Role { get; set; }
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
public float? Value { get; set; }
|
||||||
|
}
|
||||||
18
Aoyo.Taiga/WebHookTypes/TaigaPriority.cs
Normal file
18
Aoyo.Taiga/WebHookTypes/TaigaPriority.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaPriority
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public string Color { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("order")]
|
||||||
|
public int Order { get; set; }
|
||||||
|
}
|
||||||
18
Aoyo.Taiga/WebHookTypes/TaigaProject.cs
Normal file
18
Aoyo.Taiga/WebHookTypes/TaigaProject.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaProject
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("slug")]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
18
Aoyo.Taiga/WebHookTypes/TaigaSeverity.cs
Normal file
18
Aoyo.Taiga/WebHookTypes/TaigaSeverity.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaSeverity
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public string Color { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("order")]
|
||||||
|
public int Order { get; set; }
|
||||||
|
}
|
||||||
18
Aoyo.Taiga/WebHookTypes/TaigaStatus.cs
Normal file
18
Aoyo.Taiga/WebHookTypes/TaigaStatus.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaStatus
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public string Color { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_closed")]
|
||||||
|
public bool IsClosed { get; set; }
|
||||||
|
}
|
||||||
22
Aoyo.Taiga/WebHookTypes/TaigaTask.cs
Normal file
22
Aoyo.Taiga/WebHookTypes/TaigaTask.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaTask : BaseTaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("user_story")]
|
||||||
|
public TaigaUserStory? UserStory { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("milestone")]
|
||||||
|
public TaigaMilestone Milestone { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[JsonPropertyName("external_reference")]
|
||||||
|
public List<string> ExternalReference { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[JsonPropertyName("due_date")]
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("due_date_reason")]
|
||||||
|
public string DueDateReason { get; set; }
|
||||||
|
}
|
||||||
15
Aoyo.Taiga/WebHookTypes/TaigaType.cs
Normal file
15
Aoyo.Taiga/WebHookTypes/TaigaType.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaType
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public string Color { get; set; }
|
||||||
|
}
|
||||||
21
Aoyo.Taiga/WebHookTypes/TaigaUser.cs
Normal file
21
Aoyo.Taiga/WebHookTypes/TaigaUser.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaUser
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("full_name")]
|
||||||
|
public string FullName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("photo")]
|
||||||
|
public string Photo { get; set; }
|
||||||
|
}
|
||||||
45
Aoyo.Taiga/WebHookTypes/TaigaUserStory.cs
Normal file
45
Aoyo.Taiga/WebHookTypes/TaigaUserStory.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaUserStory : BaseTaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("points")]
|
||||||
|
public List<TaigaPoint> Points { get; set; } = new ();
|
||||||
|
|
||||||
|
[JsonPropertyName("total_points")]
|
||||||
|
public float? TotalPoints { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("milestone")]
|
||||||
|
public TaigaMilestone Milestone { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("client_requirement")]
|
||||||
|
public bool ClientRequirement { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("team_requirement")]
|
||||||
|
public bool TeamRequirement { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("generated_from_issue")]
|
||||||
|
public int? GeneratedFromIssue { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("generated_from_task")]
|
||||||
|
public int? GeneratedFromTask { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("from_task_ref")]
|
||||||
|
public int? FromTaskRef { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("external_reference")]
|
||||||
|
public List<string> ExternalReference { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
[JsonPropertyName("tribe_gig")]
|
||||||
|
public string TribeGig { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("kanban_order")]
|
||||||
|
public int KanbanOrder { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sprint_order")]
|
||||||
|
public int SprintOrder { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("backlog_order")]
|
||||||
|
public int BacklogOrder { get; set; }
|
||||||
|
}
|
||||||
19
Aoyo.Taiga/WebHookTypes/TaigaWebhookParser.cs
Normal file
19
Aoyo.Taiga/WebHookTypes/TaigaWebhookParser.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaWebhookParser
|
||||||
|
{
|
||||||
|
public static UserStoryWebhookPayload? ParseUserStoryWebhook(string json)
|
||||||
|
{
|
||||||
|
return System.Text.Json.JsonSerializer.Deserialize<UserStoryWebhookPayload>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IssueWebhookPayload? ParseIssueWebhook(string json)
|
||||||
|
{
|
||||||
|
return System.Text.Json.JsonSerializer.Deserialize<IssueWebhookPayload>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TaskWebhookPayload? ParseTaskWebhook(string json)
|
||||||
|
{
|
||||||
|
return System.Text.Json.JsonSerializer.Deserialize<TaskWebhookPayload>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Aoyo.Taiga/WebHookTypes/TaigaWebhookPayload.cs
Normal file
35
Aoyo.Taiga/WebHookTypes/TaigaWebhookPayload.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Aoyo.Taiga.WebHookTypes;
|
||||||
|
|
||||||
|
public class TaigaWebhookPayload<T> where T : ITaigaItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public string Action { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("by")]
|
||||||
|
public TaigaUser By { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("date")]
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("change")]
|
||||||
|
public TaigaChange? Change { get; set; }
|
||||||
|
|
||||||
|
public WebhookAction GetAction()
|
||||||
|
{
|
||||||
|
return Action?.ToLower() switch
|
||||||
|
{
|
||||||
|
"create" => WebhookAction.Create,
|
||||||
|
"change" => WebhookAction.Change,
|
||||||
|
"delete" => WebhookAction.Delete,
|
||||||
|
_ => WebhookAction.Change
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Aoyo.Taiga/WebHookTypes/WebhookAction.cs
Normal file
10
Aoyo.Taiga/WebHookTypes/WebhookAction.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Aoyo.Taiga.WebHookTypes
|
||||||
|
{
|
||||||
|
public enum WebhookAction
|
||||||
|
{
|
||||||
|
Create,
|
||||||
|
Change,
|
||||||
|
Delete,
|
||||||
|
Test
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Aoyo.Taiga/appsettings.Development.json
Normal file
8
Aoyo.Taiga/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Aoyo.Taiga/appsettings.json
Normal file
9
Aoyo.Taiga/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
201
LICENCE
Normal file
201
LICENCE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025 Dmitri Shimanski
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
78
README.md
Normal file
78
README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Aoyo.Taiga
|
||||||
|
|
||||||
|
**Aoyo.Taiga** is a lightweight C# application that listens for webhooks from [Taiga](https://taiga.io/) and sends a notification to a specified Discord channel via a bot.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- Receives webhook events from Taiga.
|
||||||
|
- Sends formatted messages to a designated Discord channel.
|
||||||
|
- Lightweight and easy to run (locally or in a container).
|
||||||
|
- Supports configuration via `environment variables` or `appsettings.json`.
|
||||||
|
|
||||||
|
## ⚙️ Required Configuration
|
||||||
|
|
||||||
|
The app requires the following configuration keys to function:
|
||||||
|
|
||||||
|
### Environment variables (`ENV`)
|
||||||
|
- `Discord__Token` – your Discord bot token.
|
||||||
|
- `Discord__Id` – target Discord channel ID.
|
||||||
|
- `Taiga__Key` – secret key to validate incoming Taiga webhooks.
|
||||||
|
|
||||||
|
### OR `appsettings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Discord": {
|
||||||
|
"Token": "your_discord_token",
|
||||||
|
"Id": "your_channel_id"
|
||||||
|
},
|
||||||
|
"Taiga": {
|
||||||
|
"Key": "your_taiga_webhook_secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Only one method of configuration is required. You can choose between env vars or appsettings.json.
|
||||||
|
🐳 Run with Docker
|
||||||
|
|
||||||
|
You can pull and run the prebuilt image from GitHub Container Registry (GHCR):
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-e Discord__Token=your_token \
|
||||||
|
-e Discord__Id=your_channel_id \
|
||||||
|
-e Taiga__Key=your_webhook_key \
|
||||||
|
-p 8080:80 \
|
||||||
|
ghcr.io/yawaflua/aoyo.taiga:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
🧪 Run Locally
|
||||||
|
|
||||||
|
Make sure you have the .NET SDK installed.
|
||||||
|
1. Clone the repo
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/yawaflua/Aoyo.Taiga.git
|
||||||
|
```
|
||||||
|
cd Aoyo.Taiga
|
||||||
|
|
||||||
|
2. Configure credentials
|
||||||
|
|
||||||
|
Either set env variables or fill in appsettings.json.
|
||||||
|
3. Run the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project Aoyo.Taiga
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, it will listen on http://localhost:8080/aoyo.
|
||||||
|
🚀 Behavior
|
||||||
|
|
||||||
|
Once a webhook is sent from Taiga (with the correct key), the app will verify it and forward a message to the Discord channel defined by the Discord__Id.
|
||||||
|
📫 Webhook Endpoint
|
||||||
|
|
||||||
|
You can point your Taiga webhook to:
|
||||||
|
|
||||||
|
`POST https://example.mycooldomain.co.il/aoyo/api/v1/TaigaWebHook/post`
|
||||||
|
|
||||||
|
Make sure to include the Taiga__Key as a query parameter or in the header to pass validation.
|
||||||
|
|
||||||
|
Project can be used under [Apache 2.0](LICENCE)
|
||||||
|
|
||||||
Reference in New Issue
Block a user