Добавьте файлы проекта.
This commit is contained in:
132
.gitea/workflows/ManualRelease.yaml
Normal file
132
.gitea/workflows/ManualRelease.yaml
Normal file
@@ -0,0 +1,132 @@
|
||||
name: Ручное создание релиза
|
||||
run-name: ${{ gitea.actor }} запустил создание релиза
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: 'Тип версии'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
default: 'patch'
|
||||
pre_release:
|
||||
description: "Отметка pre-release (для не master веток всегда true)"
|
||||
type: boolean
|
||||
default: false
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
dotnet: [ '8.0.x' ]
|
||||
|
||||
name: .Net ${{ matrix.dotnet }} Release
|
||||
steps:
|
||||
- name: Получение исходников
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Полная история для тегов
|
||||
|
||||
- name: Автоматическое определение pre-release
|
||||
id: pre-release-detector
|
||||
run: |
|
||||
# Определяем ветку по умолчанию (main/master)
|
||||
DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | awk '{print $3}')
|
||||
echo "Default branch: $DEFAULT_BRANCH"
|
||||
|
||||
# Если текущая ветка не дефолтная - форсируем pre-release
|
||||
if [ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]; then
|
||||
echo "Ветка не master - устанавливаем pre-release"
|
||||
echo "PRE_RELEASE=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Ветка master - используем pre-release из параметров"
|
||||
echo "PRE_RELEASE=${{ github.event.inputs.pre_release }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Получение последней версии
|
||||
id: versioning
|
||||
run: |
|
||||
# Получаем последний тег
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
echo "Последний тэг: $LATEST_TAG"
|
||||
|
||||
# Извлекаем цифры версии
|
||||
VERSION="${LATEST_TAG#v}"
|
||||
|
||||
if [[ ! $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.+)?$ ]]; then
|
||||
echo "Неверный формат версии: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
|
||||
# Увеличиваем версию
|
||||
case "${{ github.event.inputs.version_type }}" in
|
||||
major)
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
;;
|
||||
minor)
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
;;
|
||||
patch)
|
||||
PATCH=$((PATCH + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
BASE_VERSION="$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
# Добавим pre-release, если требуется
|
||||
if ${{ env.PRE_RELEASE }}; then
|
||||
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
|
||||
NEW_VERSION="$BASE_VERSION-pre.$TIMESTAMP"
|
||||
else
|
||||
NEW_VERSION="$BASE_VERSION"
|
||||
fi
|
||||
|
||||
echo "Новая версия: $NEW_VERSION"
|
||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Настройка .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ matrix.dotnet }}
|
||||
|
||||
- name: Установка версии RetailUpdatesBot
|
||||
run: |
|
||||
sed -i "s/<Version>.*<\/Version>/<Version>${{ env.VERSION }}<\/Version>/g" ./RetailUpdatesBot/RetailUpdatesBot.csproj
|
||||
|
||||
- name: Восстановление зависимостей
|
||||
run: dotnet restore --nologo
|
||||
|
||||
- name: Сборка решения
|
||||
run: dotnet build --no-restore --nologo
|
||||
|
||||
- name: Публикация
|
||||
run: dotnet publish RetailUpdatesBot --configuration Release --runtime win-x64 --artifacts-path artifacts --nologo
|
||||
|
||||
- name: Создание ZIP архива
|
||||
run: |
|
||||
cd artifacts/publish/RetailUpdatesBot/release_win-x64/
|
||||
zip -r ../RetailUpdatesBot-v${{ env.VERSION }}.zip *
|
||||
cd ..
|
||||
echo "ZIP_PATH=RetailUpdatesBot-v${{ env.VERSION }}.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Создание релиза
|
||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
||||
with:
|
||||
files: artifacts/publish/RetailUpdatesBot/RetailUpdatesBot-v${{ env.VERSION }}.zip
|
||||
tag_name: v${{ env.VERSION }}
|
||||
name: RetailUpdatesBot-v${{ env.VERSION }}
|
||||
body: "## Что нового\n\n- Описание изменений\n- Функциональность\n- Исправления"
|
||||
prerelease: ${{ env.PRE_RELEASE }}
|
||||
22
Directory.Build.props
Normal file
22
Directory.Build.props
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Общие настройки -->
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
|
||||
|
||||
<!-- Метаданные для NuGet -->
|
||||
<Authors>FrigaT</Authors>
|
||||
<Product>PipelineFramework</Product>
|
||||
<Description>Гибкий и лёгкий фреймворк конвейеров для .NET-приложений</Description>
|
||||
<PackageProjectUrl>https://github.com/MaxLabs/PipelineFramework</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/MaxLabs/PipelineFramework</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageTags>pipeline;framework;middleware;dotnet</PackageTags>
|
||||
<Version>0.0.1</Version>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
43
PipelineFramework.sln
Normal file
43
PipelineFramework.sln
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36414.22
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineFramework", "src\PipelineFramework\PipelineFramework.csproj", "{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineFramework.DI", "src\PipelineFramework.DI\PipelineFramework.DI.csproj", "{B5D47E22-BD13-458B-ACDC-2017C962AE80}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Directory.Build.props = Directory.Build.props
|
||||
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
|
||||
{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5D47E22-BD13-458B-ACDC-2017C962AE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5D47E22-BD13-458B-ACDC-2017C962AE80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5D47E22-BD13-458B-ACDC-2017C962AE80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5D47E22-BD13-458B-ACDC-2017C962AE80}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{4407AD88-6FFC-47CF-B06A-BC1D9E05B7E5} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
{B5D47E22-BD13-458B-ACDC-2017C962AE80} = {36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {22A2D53A-92B8-4C82-A391-6A00DE84526C}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# PipelineFramework
|
||||
|
||||
**PipelineFramework** — это гибкий и лёгкий фреймворк для построения конвейеров обработки данных и логики в .NET-приложениях.
|
||||
Он позволяет создавать последовательности шагов (middleware), которые обрабатывают входные данные, управляют потоком выполнения и обеспечивают расширяемость.
|
||||
|
||||
## 🚀 Возможности
|
||||
|
||||
- Простое определение шагов конвейера
|
||||
- Поддержка асинхронной обработки
|
||||
- Встроенная DI-интеграция
|
||||
- Расширяемость через интерфейсы
|
||||
- Минимум зависимостей
|
||||
|
||||
## 📦 Установка
|
||||
|
||||
```bash
|
||||
dotnet add package PipelineFramework
|
||||
```
|
||||
|
||||
## 🧩 Пример использования
|
||||
|
||||
```csharp
|
||||
var pipeline = new PipelineBuilder<string>()
|
||||
.Use(async (input, next) =>
|
||||
{
|
||||
Console.WriteLine($"Step 1: {input}");
|
||||
await next(input + " → Step1");
|
||||
})
|
||||
.Use(async (input, next) =>
|
||||
{
|
||||
Console.WriteLine($"Step 2: {input}");
|
||||
await next(input + " → Step2");
|
||||
})
|
||||
.Build();
|
||||
|
||||
await pipeline.ExecuteAsync("Start");
|
||||
```
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
- [Примеры использования](docs/examples.md)
|
||||
- [Интеграция с DI](docs/di.md)
|
||||
- [Расширение фреймворка](docs/extending.md)
|
||||
|
||||
## 🛠 Требования
|
||||
|
||||
- .NET 9.0 или выше
|
||||
|
||||
## 🧑💻 Автор
|
||||
|
||||
Разработано [FrigaT](https://github.com/FrigaT)
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
Проект распространяется под лицензией [MIT](LICENSE)
|
||||
6
src/PipelineFramework.Abstractions/IPipeline.cs
Normal file
6
src/PipelineFramework.Abstractions/IPipeline.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace PipelineFramework.Abstractions;
|
||||
|
||||
public interface IPipeline<TContext>
|
||||
{
|
||||
Task ExecuteAsync(TContext context);
|
||||
}
|
||||
7
src/PipelineFramework.Abstractions/IPipelineHook.cs
Normal file
7
src/PipelineFramework.Abstractions/IPipelineHook.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PipelineFramework.Abstractions;
|
||||
|
||||
public interface IPipelineHook<TContext>
|
||||
{
|
||||
Task OnBeforeExecuteAsync(TContext context);
|
||||
Task OnAfterExecuteAsync(TContext context);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PipelineFramework.Abstractions;
|
||||
|
||||
public interface IPipelineMiddleware<TContext>
|
||||
{
|
||||
Task InvokeAsync(TContext context, Func<Task> next);
|
||||
}
|
||||
8
src/PipelineFramework.Abstractions/IPipelineModule.cs
Normal file
8
src/PipelineFramework.Abstractions/IPipelineModule.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace PipelineFramework.Abstractions;
|
||||
|
||||
public interface IPipelineModule<TContext>
|
||||
{
|
||||
string Id { get; }
|
||||
IEnumerable<string> DependsOn { get; }
|
||||
Task ExecuteAsync(TContext context);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
17
src/PipelineFramework.DI/PipelineFramework.DI.csproj
Normal file
17
src/PipelineFramework.DI/PipelineFramework.DI.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PipelineFramework\PipelineFramework.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
using PipelineFramework;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class PipelineServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddPipeline<TContext>(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IPipeline<TContext>>(provider =>
|
||||
{
|
||||
var modules = provider.GetServices<IPipelineModule<TContext>>().ToList();
|
||||
var middlewares = provider.GetServices<IPipelineMiddleware<TContext>>().ToList();
|
||||
var hooks = provider.GetServices<IPipelineHook<TContext>>().ToList();
|
||||
|
||||
return new Pipeline<TContext>(modules, middlewares, hooks);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
6
src/PipelineFramework/Abstractions/IPipeline.cs
Normal file
6
src/PipelineFramework/Abstractions/IPipeline.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public interface IPipeline<TContext>
|
||||
{
|
||||
Task RunAsync(TContext context);
|
||||
}
|
||||
8
src/PipelineFramework/Abstractions/IPipelineHook.cs
Normal file
8
src/PipelineFramework/Abstractions/IPipelineHook.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public interface IPipelineHook<TContext>
|
||||
{
|
||||
Task OnBeforeAsync(TContext context, IPipelineModule<TContext> module);
|
||||
Task OnAfterAsync(TContext context, IPipelineModule<TContext> module);
|
||||
Task OnErrorAsync(TContext context, IPipelineModule<TContext> module, Exception ex);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public interface IPipelineMiddleware<TContext>
|
||||
{
|
||||
Task InvokeAsync(TContext context, Func<Task> next);
|
||||
}
|
||||
9
src/PipelineFramework/Abstractions/IPipelineModule.cs
Normal file
9
src/PipelineFramework/Abstractions/IPipelineModule.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public interface IPipelineModule<TContext>
|
||||
{
|
||||
string Id { get; }
|
||||
IEnumerable<string> DependsOn { get; }
|
||||
|
||||
Task ExecuteAsync(TContext context);
|
||||
}
|
||||
70
src/PipelineFramework/Core/Pipeline.cs
Normal file
70
src/PipelineFramework/Core/Pipeline.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public class Pipeline<TContext> : IPipeline<TContext>
|
||||
{
|
||||
private readonly List<IPipelineModule<TContext>> _modules;
|
||||
private readonly List<IPipelineMiddleware<TContext>> _middlewares;
|
||||
private readonly List<IPipelineHook<TContext>> _hooks;
|
||||
|
||||
|
||||
public Pipeline(
|
||||
List<IPipelineModule<TContext>> modules,
|
||||
List<IPipelineMiddleware<TContext>> middlewares,
|
||||
List<IPipelineHook<TContext>> hooks)
|
||||
{
|
||||
_modules = modules;
|
||||
_middlewares = middlewares;
|
||||
_hooks = hooks;
|
||||
}
|
||||
|
||||
public async Task RunAsync(TContext context)
|
||||
{
|
||||
var executed = new HashSet<string>();
|
||||
var moduleMap = _modules.ToDictionary(m => m.Id);
|
||||
|
||||
Func<Task> pipeline = async () =>
|
||||
{
|
||||
while (executed.Count < _modules.Count)
|
||||
{
|
||||
var ready = _modules
|
||||
.Where(m => !executed.Contains(m.Id) &&
|
||||
m.DependsOn.All(dep => executed.Contains(dep)))
|
||||
.ToList();
|
||||
|
||||
var tasks = ready.Select(m => ExecuteModuleWithHooksAsync(m, context)).ToList();
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
foreach (var m in ready)
|
||||
executed.Add(m.Id);
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var middleware in _middlewares.Reverse())
|
||||
{
|
||||
var next = pipeline;
|
||||
pipeline = () => middleware(context, next);
|
||||
}
|
||||
|
||||
await pipeline();
|
||||
}
|
||||
|
||||
private async Task ExecuteModuleWithHooksAsync(IPipelineModule<TContext> module, TContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var hook in _hooks)
|
||||
await hook.OnBeforeAsync(context, module);
|
||||
|
||||
await module.ExecuteAsync(context);
|
||||
|
||||
foreach (var hook in _hooks)
|
||||
await hook.OnAfterAsync(context, module);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
foreach (var hook in _hooks)
|
||||
await hook.OnErrorAsync(context, module, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/PipelineFramework/Core/PipelineBuilder.cs
Normal file
38
src/PipelineFramework/Core/PipelineBuilder.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace PipelineFramework;
|
||||
|
||||
public abstract class PipelineBuilder<TContext>
|
||||
{
|
||||
private readonly List<IPipelineModule<TContext>> _modules = new();
|
||||
private readonly List<Func<TContext, Func<Task>, Task>> _middlewares = new();
|
||||
private readonly List<IPipelineHook<TContext>> _hooks = new();
|
||||
|
||||
public PipelineBuilder<TContext> AddModule(IPipelineModule<TContext> module)
|
||||
{
|
||||
_modules.Add(module);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PipelineBuilder<TContext> UseMiddleware<T>() where T : IPipelineMiddleware<TContext>, new()
|
||||
{
|
||||
_middlewares.Add((ctx, next) => new T().InvokeAsync(ctx, next));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PipelineBuilder<TContext> UseMiddleware(Func<TContext, Func<Task>, Task> middleware)
|
||||
{
|
||||
_middlewares.Add(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PipelineBuilder<TContext> AddHook(IPipelineHook<TContext> hook)
|
||||
{
|
||||
_hooks.Add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected IReadOnlyList<IPipelineModule<TContext>> Modules => _modules;
|
||||
protected IReadOnlyList<Func<TContext, Func<Task>, Task>> Middlewares => _middlewares;
|
||||
protected IReadOnlyList<IPipelineHook<TContext>> Hooks => _hooks;
|
||||
|
||||
public abstract IPipeline<TContext> Build();
|
||||
}
|
||||
9
src/PipelineFramework/PipelineFramework.csproj
Normal file
9
src/PipelineFramework/PipelineFramework.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user