From 418e09bd4de7acf51f6f4238a979f0f006817423 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Fri, 10 Apr 2026 16:22:30 +0200 Subject: [PATCH 1/3] fix: update Build and Deploy badge to point to ci.yml The workflow was renamed from master_xposterfunction.yml to ci.yml. The badge URL in README.md was never updated, causing it to render as broken. Fixes #82. --- README.md | 950 +----------------------------------------------------- 1 file changed, 1 insertion(+), 949 deletions(-) diff --git a/README.md b/README.md index 5b0dc08..fb2cd80 100644 --- a/README.md +++ b/README.md @@ -1,949 +1 @@ -# XPoster πŸš€ - -[![Azure Functions](https://img.shields.io/badge/Azure%20Functions-v4-0062AD?logo=azurefunctions&logoColor=white)](https://azure.microsoft.com/en-us/services/functions/) -[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) -[![C#](https://img.shields.io/badge/C%23-12.0-239120?logo=csharp&logoColor=white)](https://docs.microsoft.com/en-us/dotnet/csharp/) -[![OpenAI](https://img.shields.io/badge/OpenAI-Powered-412991?logo=openai&logoColor=white)](https://openai.com/) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Deployment](https://img.shields.io/badge/Deployed-Azure-blue)](https://xposterfunction.azurewebsites.net/) -[![Build and Deploy](https://github.com/artcava/XPoster/actions/workflows/master_xposterfunction.yml/badge.svg)](https://github.com/artcava/XPoster/actions/workflows/master_xposterfunction.yml) - -> **AI-Powered Social Media Automation Platform** -> -> XPoster is an Azure Function that automates content publishing across multiple social media platforms (Twitter/X, LinkedIn, Instagram) using artificial intelligence for content generation and curation. - ---- - -## πŸ“‹ Table of Contents - -- [Features](#features) -- [Architecture](#architecture) -- [Technologies](#technologies) -- [Getting Started](#getting-started) -- [Configuration](#configuration) -- [Deployment](#deployment) -- [Usage](#usage) -- [Scheduling](#scheduling) -- [Extensibility](#extensibility) -- [Testing](#testing) -- [Monitoring](#monitoring) -- [Roadmap](#roadmap) -- [Contributing](#contributing) -- [License](#license) - -> πŸ“ For a deep-dive into architectural decisions, design patterns, ADRs, and extension contracts, see [ARCHITECTURE.md](ARCHITECTURE.md). - ---- - -## Features - -### πŸ€– Content Generation -- **AI-Powered Summarization**: Intelligent RSS feed summaries using gpt-4.1-nano -- **Image Generation**: Automatic contextual image creation with gpt-image-1.5 -- **Smart Hashtags**: Automatic keyword conversion to optimized hashtags -- **Multi-Strategy**: Support for different content generation algorithms - -### 🌐 Multi-Platform Publishing -- **Twitter/X**: Automated posting with image support -- **LinkedIn**: Posts to a personal LinkedIn profile via the UGC Posts API. Company page support is planned (see issue #XX). -- **Instagram**: Publishing via Graph API (in development, see issue #XX for production readiness checklist) - -### βš™οΈ Automation & Scheduling -- **Timer-Based Execution**: Configurable automatic execution -- **Smart Scheduling**: Different posting strategies based on time -- **Conditional Logic**: Publishing only when appropriate -- **Flexible Configuration**: Customizable schedule via environment variables - -### πŸ“Š Enterprise Features -- **Application Insights**: Complete monitoring and telemetry -- **Structured Logging**: Detailed logs for debugging and audit -- **Error Handling**: Robust error management with retry logic -- **Dependency Injection**: Modular and testable architecture - ---- - -## Architecture - -> πŸ“ For the full architectural rationale, ADRs, design patterns, and Mermaid data-flow diagram, see **[ARCHITECTURE.md](ARCHITECTURE.md)**. - -### High-Level Overview - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Azure Timer Trigger β”‚ -β”‚ (configurable schedule) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Generator Factory β”‚ ◄─── Strategy Pattern -β”‚ (Time-based Selector) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Feed β”‚ β”‚ PowerLaw β”‚ β”‚ No β”‚ -β”‚Generator β”‚ β”‚Generator β”‚ β”‚Generator β”‚ -β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Services β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ β€’ AI Service β”‚ ◄─── OpenAI Integration - β”‚ β€’ Feed Service β”‚ ◄─── RSS Parser - β”‚ β€’ Crypto Svc β”‚ ◄─── Security Utils - β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Sender Plugins β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ β€’ XSender β”‚ ◄─── Twitter/X API - β”‚ β€’ InSender β”‚ ◄─── LinkedIn API - β”‚ β€’ IgSender β”‚ ◄─── Instagram API - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Core Components - -#### 1. **XFunction** (Entry Point) -Timer-triggered Azure Function that orchestrates the entire publishing workflow. - -**Cron Expression**: Configurable via environment variable (default: `0 5 * * * *`) - -#### 2. **GeneratorFactory** (Factory + Strategy Pattern) -Dynamically selects the appropriate generator based on current time. - -| Time | Platform | Strategy | -|------|----------|----------| -| 06:00 | LinkedIn | Feed Summary | -| 08:00 | Twitter/X | Feed Summary | -| 14:00 | LinkedIn | Power Law | -| 16:00 | Twitter/X | Power Law | - -#### 3. **Generators** (Content Strategy) -- **FeedGenerator**: Analyzes crypto RSS feeds, generates AI summaries, creates images -- **PowerLawGenerator**: Generates content based on statistical distribution -- **NoGenerator**: Placeholder for time slots without publishing - -#### 4. **Services Layer** -- **AiService**: Interface with OpenAI (gpt-4.1-nano, gpt-image-1.5) -- **FeedService**: RSS parser with caching and intelligent filtering -- **CryptoService**: Crypto-currencies utilities - -#### 5. **Sender Plugins** (Platform Abstraction) -- **XSender**: Twitter/X via LinqToTwitter -- **InSender**: LinkedIn via HTTP API -- **IgSender**: Instagram via Graph API (in development) - ---- - -## Technologies - -### Core Framework -- **.NET 8.0** - Main framework -- **Azure Functions v4** - Serverless compute -- **C# 12** - Programming language - -### AI & ML -- **OpenAI** - gpt-4.1-nano for summarization -- **gpt-image-1.5** - Image generation - -### Social Media APIs -- **LinqToTwitter 6.15.0** - Twitter/X integration -- **LinkedIn REST API v2** - LinkedIn publishing -- **Instagram Graph API** - Instagram (in development) - -### Monitoring & Logging -- **Application Insights** - Telemetry and monitoring -- **ILogger** - Structured logging - -### Utilities -- **System.ServiceModel.Syndication** - RSS parsing -- **Microsoft.Extensions.Http** - HTTP client factory - ---- - -## Getting Started - -### Prerequisites - -- **.NET 8.0 SDK** ([Download](https://dotnet.microsoft.com/download/dotnet/8.0)) -- **Azure Functions Core Tools** ([Install](https://docs.microsoft.com/azure/azure-functions/functions-run-local)) -- **Visual Studio 2022** or **Visual Studio Code** -- **Azure Account** (with active subscription) -- **OpenAI API** (with gpt-4.1-nano and gpt-image-1.5 enabled) - -### Clone the Repository - -```bash -git clone https://github.com/artcava/XPoster.git -cd XPoster -``` - -### Restore Dependencies - -```bash -dotnet restore -``` - -### Build the Project - -```bash -dotnet build -``` - -### Run Tests - -```bash -dotnet test -``` - -### Configure Local Settings - -A template file with all required keys and inline documentation is versioned at [`src/local.settings.json.example`](src/local.settings.json.example). - -Copy it and fill in your credentials before running the function locally: - -```bash -cp src/local.settings.json.example src/local.settings.json -``` - -Then open `src/local.settings.json` and replace every empty string `""` with the actual value for each service. See the [Configuration](#configuration) section for details on where to obtain each credential. - -> ⚠️ `local.settings.json` is listed in `.gitignore` and will **never** be committed. The `.example` variant is safe to version because it contains no real secrets. - -> πŸ“– For the full expanded setup guide with troubleshooting tips, see [docs/getting-started.md](docs/getting-started.md). - ---- - -## Configuration - -### 1. Local Development - -Create a `local.settings.json` file in the `src/` directory: - -```json -{ - "IsEncrypted": false, - "Values": { - "CronSchedule": "0 5 * * * *", - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", - - "X_API_KEY": "your_twitter_api_key", - "X_API_SECRET": "your_twitter_api_secret", - "X_ACCESS_TOKEN": "your_twitter_access_token", - "X_ACCESS_TOKEN_SECRET": "your_twitter_access_token_secret", - - "IN_ACCESS_TOKEN": "your_linkedin_token", - "IN_OWNER": "your_linkedin_owner_id", - - "IG_ACCESS_TOKEN": "your_instagram_token", - "IG_ACCOUNT_ID": "your_instagram_account_id", - -<<<<<<< develop - "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/", - "AZURE_OPENAI_KEY": "your_openai_key", - "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4.1-nano" -======= - "OPENAI_API_KEY": "your_openai_api_key" ->>>>>>> master - } -} -``` - -> πŸ“– Full configuration reference with types, defaults, and where to obtain each credential: [docs/configuration.md](docs/configuration.md). - -### 2. Azure Configuration - -#### App Settings (Azure Portal) - -Navigate to **Azure Portal** β†’ **Function App** β†’ **Configuration** β†’ **Application Settings** - -Add the same variables from `local.settings.json`. - -#### Managed Identity (Recommended) - -For enhanced security, use Azure Managed Identity: - -1. Enable **System Assigned Managed Identity** on the Function App -2. Assign appropriate roles on: - - Azure OpenAI Service - - Azure Key Vault (for secrets) -3. Modify `Program.cs` to use `DefaultAzureCredential` - -```csharp -builder.Services.AddSingleton(sp => -{ - var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")); - return new OpenAIClient(endpoint, new DefaultAzureCredential()); -}); -``` - ---- - -## Deployment - -### Option 1: GitHub Actions (Automated CI/CD) - -The repository includes a GitHub Actions workflow (`.github/workflows/master_xposterfunction.yml`). - -**Setup**: -1. Create a Function App in Azure Portal -2. Download the **Publish Profile** from the Function App -3. Add the content as a **Secret** in GitHub: - - Name: `AZURE_FUNCTIONAPP_PUBLISH_PROFILE` -4. Every push to `master` triggers automatic deployment - -### Option 2: Azure CLI - -```bash -# Login -az login - -# Create Resource Group -az group create --name XPosterRG --location westeurope - -# Create Storage Account -az storage account create \ - --name xposterstorage \ - --resource-group XPosterRG \ - --location westeurope \ - --sku Standard_LRS - -# Create Function App -az functionapp create \ - --name xposterfunction \ - --resource-group XPosterRG \ - --consumption-plan-location westeurope \ - --runtime dotnet-isolated \ - --runtime-version 8 \ - --functions-version 4 \ - --storage-account xposterstorage - -# Deploy -cd src -func azure functionapp publish xposterfunction -``` - -### Option 3: Visual Studio - -1. Right-click on the `XPoster` project -2. Select **Publish** -3. Choose **Azure** β†’ **Azure Function App (Windows)** -4. Select or create a Function App -5. Click **Publish** - -> πŸ“– Step-by-step guide with post-deployment checklist: [docs/deployment.md](docs/deployment.md). - ---- - -## Usage - -### Local Execution - -```bash -cd src -func start -``` - -The function will run locally according to the configured cron expression. - -### Manual Trigger (Azure Portal) - -1. Go to **Azure Portal** β†’ **Function App** β†’ **Functions** -2. Select `XPosterFunction` -3. Click **Test/Run** -4. Click **Run** - -### HTTP Trigger (Optional) - -Add an HTTP trigger for testing: - -```csharp -[Function("XPosterHttpTrigger")] -public async Task RunHttp( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) -{ - await Run(null); - var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteStringAsync("XPoster executed successfully"); - return response; -} -``` - ---- - -## Scheduling - -### Schedule Configuration - -The execution frequency is configurable via the `CronSchedule` environment variable: - -**Format**: 6-field cron expression: `{second} {minute} {hour} {day} {month} {dayOfWeek}` - -**Configuration**: - - -```json -//local.settings.json -{ - "Values": { - "CronSchedule": "0 5 * * * *" - } -} -``` - -```bash -//Azure CLI -az functionapp config appsettings set ---name xposterfunction ---resource-group XPosterRG ---settings "CronSchedule=0 5 * * * *" -``` - -### Cron Expression Examples - -| Schedule | Cron Expression | Description | -|----------|-----------------|-------------| -| **Default** | `0 5 */2 * * *` | Every 2 hours at :05 | -| **Hourly** | `0 0 * * * *` | Every hour on the hour | -| **Every 4 hours** | `0 0 */4 * * *` | Every 4 hours | -| **Business Hours** | `0 0 9,12,15,18 * * 1-5` | 9, 12, 15, 18 (Mon-Fri) | -| **Morning/Evening** | `0 0 8,20 * * *` | At 8:00 and 20:00 | -| **Daily** | `0 0 9 * * *` | Every day at 9:00 | -| **Quick Test** | `*/30 * * * * *` | Every 30 seconds (dev only) | - -### Time-based Strategy (GeneratorFactory) - -Modify `GeneratorFactory.cs` to customize which generator to use at each hour: - -```csharp -private static readonly Dictionary sendParameters = new() -{ -{ 6, MessageSender.InSummaryFeed }, // LinkedIn Feed -{ 8, MessageSender.XSummaryFeed }, // Twitter Feed -{ 10, MessageSender.IgSummaryFeed }, // Instagram Feed (enable when ready) -{ 14, MessageSender.InPowerLaw }, // LinkedIn Power Law -{ 16, MessageSender.XPowerLaw }, // Twitter Power Law -{ 18, MessageSender.IgPowerLow }, // Instagram Power Law -}; -``` ---- - -### Best Practices - -βœ… **Testing**: Use frequent schedules in development (`*/5 * * * * *` = every 5 secs) -βœ… **Production**: More conservative schedules to avoid rate limiting -βœ… **Multi-environment**: Different schedules for Dev/Staging/Prod -βœ… **Monitoring**: Check logs to confirm correct execution - ---- - -## Extensibility - -### Adding a New Platform - -**1. Create the Sender Plugin** - -```csharp -// src/SenderPlugins/TikTokSender.cs -public class TikTokSender : ISender -{ - public int MessageMaxLenght => 150; - - public async Task SendAsync(Post post) - { - // Implement TikTok API logic - return true; - } -} -``` - -**2. Register in DI Container** - -```csharp -// src/Program.cs -builder.Services.AddTransient(); -``` - -**3. Add Enum** - -```csharp -// src/Abstraction/Enums.cs -public enum MessageSender -{ - // ... - TikTokSummaryFeed, -} -``` - -**4. Configure Factory** - -```csharp -// src/Implementation/GeneratorFactory.cs -case MessageSender.TikTokSummaryFeed: - return GetInstance( - _serviceProvider.GetService(typeof(TikTokSender)) as ISender - ); -``` - -> πŸ“– Full extension guide with services and design constraints: [docs/extending-xposter.md](docs/extending-xposter.md). - -### Adding a New Generator - -```csharp -// src/Implementation/QuoteGenerator.cs -public class QuoteGenerator : BaseGenerator -{ - public override async Task? GenerateAsync() - { - // Logic to generate motivational quotes - var quote = await _aiService.GetQuoteAsync(); - return new Post { Content = quote }; - } -} -``` - ---- - -## Testing - -### Test Structure - -``` -tests/ -β”œβ”€β”€ Abstraction/ # tests for src/Abstraction/ -β”œβ”€β”€ Implementation/ # tests for src/Implementation/ (FeedGenerator, PowerLawGenerator, GeneratorFactory…) -β”œβ”€β”€ Models/ # tests for src/Models/ -β”œβ”€β”€ SenderPlugins/ # tests for src/SenderPlugins/ (XSender, InSender, IgSender…) -β”œβ”€β”€ Services/ # tests for src/Services/ (AiService, FeedService, CryptoService…) -β”œβ”€β”€ XFunctionMissingBranchTests.cs -β”œβ”€β”€ XFunctionTests.cs # integration-level tests for XFunction -└── XPoster.Tests.csproj -``` - -### Running Tests - -```bash -# All tests -dotnet test - -# Specific tests -dotnet test --filter "FullyQualifiedName~FeedGenerator" - -# With coverage -dotnet test --collect:"XPlat Code Coverage" -``` - -> πŸ“– Full testing strategy, mocking patterns, and coverage goals: [tests/README.md](tests/README.md). - -### Mocking External Services - -```csharp -[Fact] -public async Task FeedGenerator_ShouldGenerateSummary() -{ - // Arrange - var mockAiService = new Mock(); - mockAiService - .Setup(x => x.GetSummaryAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync("Test summary"); - - var generator = new FeedGenerator( - mockSender.Object, - mockLogger.Object, - mockFeedService.Object, - mockAiService.Object - ); - - // Act - var result = await generator.GenerateAsync(); - - // Assert - Assert.NotNull(result); - Assert.Contains("Test summary", result.Content); -} -``` - ---- - -## Monitoring - -### Application Insights Setup - -#### 1. Create the Application Insights Resource - -1. In the **Azure Portal**, search for **Application Insights** and click **Create** -2. Fill in the details: - - **Name**: e.g. `xposter-appinsights` - - **Resource Group**: same as your Function App (`XPosterRG`) - - **Region**: same region as the Function App - - **Resource Mode**: Workspace-based (recommended) -3. Click **Review + Create**, then **Create** -4. Once created, navigate to the resource and copy the **Connection String** (shown on the Overview blade) - -#### 2. Link Application Insights to the Function App - -Add the connection string as an **Application Setting** in the Function App: - -**Via Azure Portal**: -1. Go to **Function App** β†’ **Configuration** β†’ **Application Settings** -2. Click **+ New application setting** -3. Name: `APPLICATIONINSIGHTS_CONNECTION_STRING` -4. Value: paste the full connection string copied above -5. Click **Save** and confirm the restart - -**Via Azure CLI**: -```bash -az functionapp config appsettings set \ - --name xposterfunction \ - --resource-group XPosterRG \ - --settings "APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=;IngestionEndpoint=https://.in.applicationinsights.azure.com/" -``` - -#### 3. SDK Wiring in Program.cs - -The `Microsoft.Azure.Functions.Worker.ApplicationInsights` package is used. It is automatically registered when the connection string is present in the environment. No explicit SDK code is required in `Program.cs` for Azure Functions v4 isolated worker beyond the standard host builder: - -```csharp -// Program.cs β€” Application Insights is enabled automatically -// when APPLICATIONINSIGHTS_CONNECTION_STRING is set. -var host = new HostBuilder() - .ConfigureFunctionsWebApplication() - .ConfigureServices(services => - { - services.AddApplicationInsightsTelemetryWorkerService(); - services.ConfigureFunctionsApplicationInsights(); - // ... other registrations - }) - .Build(); -``` - -#### 4. Connection String Configuration - -Add the following key to `local.settings.json` for local telemetry (optional but recommended for debugging): - -```json -{ - "IsEncrypted": false, - "Values": { - "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=;IngestionEndpoint=https://.in.applicationinsights.azure.com/" - } -} -``` - -> ⚠️ The key is already included in [`src/local.settings.json.example`](src/local.settings.json.example). See [#29](https://github.com/artcava/XPoster/issues/29) for the full settings template. - ---- - -### Key Metrics - -- **Execution Count**: Number of function executions -- **Success Rate**: % of successful executions -- **Average Duration**: Average execution time -- **AI Token Usage**: OpenAI token consumption - ---- - -### KQL Queries - -All queries below are verified against the Azure Functions v4 isolated worker table schema (`requests`, `traces`, `dependencies`). - -```kql -// Executions last 24h -requests -| where timestamp > ago(24h) -| where name == "XPosterFunction" -| summarize count() by bin(timestamp, 1h) -| render timechart - -// Error rate (severity >= 3 = Warning+) -traces -| where timestamp > ago(7d) -| where severityLevel >= 3 -| summarize errorCount = count() by bin(timestamp, 1d) -| render barchart - -// AI Cost Tracking -dependencies -| where timestamp > ago(30d) -| where target contains "openai" -| extend tokenUsage = toint(customDimensions.tokenCount) -| summarize totalTokens = sum(tokenUsage), totalCost = sum(tokenUsage) * 0.00006 -``` - -> πŸ’‘ **Tip**: To pin any query result to an Azure Dashboard, run it in the **Logs** blade, click the **Pin to dashboard** icon (πŸ“Œ) in the top-right corner of the results panel, choose your dashboard, and click **Pin**. - ---- - -### Live Metrics (Local Development) - -Application Insights **Live Metrics** streams telemetry in near real-time with sub-second latency β€” useful to verify the function is behaving correctly during local development. - -1. Start the function locally: - ```bash - cd src - func start - ``` -2. In the Azure Portal, open your **Application Insights** resource -3. Click **Live Metrics** in the left-hand menu -4. Trigger a function execution (timer fires automatically, or use an HTTP trigger) -5. Observe incoming requests, dependency calls, exceptions, and custom traces in real time - -> ℹ️ Live Metrics works even in local development as long as `APPLICATIONINSIGHTS_CONNECTION_STRING` is set in `local.settings.json`. - ---- - -### Alerting Configuration - -#### Step-by-Step: Create an Alert via Azure Portal - -The following example creates an alert for **more than 3 consecutive errors within 1 hour**: - -1. In the Azure Portal, navigate to your **Application Insights** resource -2. Select **Alerts** β†’ **+ Create** β†’ **Alert rule** -3. **Scope**: confirm it points to the Application Insights resource -4. **Condition**: - - Click **+ Add condition** - - Signal type: **Custom log search** - - Enter the following KQL query: - ```kql - traces - | where severityLevel >= 3 - | where timestamp > ago(1h) - | summarize errorCount = count() - ``` - - Alert logic: **Greater than** threshold **3** - - Evaluation frequency: `5 minutes` - - Lookback period: `1 hour` -5. **Actions**: - - Click **+ Add action group** β†’ **Create action group** - - Add a notification: type **Email/SMS/Push/Voice**, fill in your email - - Optionally add a **Webhook** action (e.g. to a Slack/Teams incoming webhook URL) -6. **Details**: - - Severity: **2 – Warning** - - Alert rule name: `XPoster - Consecutive Errors` -7. Click **Review + Create** - -#### Recommended Alert Rules - -| Alert | KQL signal | Threshold | Severity | -|-------|-----------|-----------|----------| -| Consecutive errors | `traces \| where severityLevel >= 3` | > 3 in 1h | Sev 2 – Warning | -| Token budget exceeded | `dependencies \| where target contains "openai" \| extend t = toint(customDimensions.tokenCount) \| summarize sum(t)` | > monthly budget | Sev 2 – Warning | -| High latency | `requests \| where name == "XPosterFunction" \| summarize avg(duration)` | > 60 000 ms | Sev 3 – Informational | -| Function downtime | Built-in **Availability** test on the Function App URL | < 100% | Sev 1 – Error | - -#### IaC: Bicep Snippet for Alert Provisioning - -Use the following Bicep snippet to provision the consecutive-errors alert rule as Infrastructure-as-Code: - -```bicep -resource consecutiveErrorsAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { - name: 'XPoster-ConsecutiveErrors' - location: resourceGroup().location - properties: { - description: 'Fires when more than 3 errors are logged within 1 hour' - severity: 2 - enabled: true - scopes: [ - appInsights.id - ] - evaluationFrequency: 'PT5M' - windowSize: 'PT1H' - criteria: { - allOf: [ - { - query: 'traces | where severityLevel >= 3 | summarize errorCount = count()' - timeAggregation: 'Count' - operator: 'GreaterThan' - threshold: 3 - failingPeriods: { - numberOfEvaluationPeriods: 1 - minFailingPeriodsToAlert: 1 - } - } - ] - } - actions: { - actionGroups: [ - actionGroup.id - ] - } - } -} -``` - -> πŸ“– Full KQL queries, alert thresholds, and live debugging instructions: [docs/monitoring.md](docs/monitoring.md). - ---- - -## Roadmap - -### βœ… Phase 1: Foundation (Complete) -- [x] Azure Function setup -- [x] Multi-platform sender architecture -- [x] AI integration (gpt-4.1-nano, gpt-image-1.5) -- [x] Twitter/X publishing -- [x] LinkedIn publishing -- [x] RSS feed parsing -- [x] CI/CD pipeline - -### 🚧 Phase 2: Stabilization (In Progress) -- [ ] AI migration to Azure Foundry -- [ ] Linkedin auto-update authorization token -- [ ] Configuration externalization -- [ ] Enhanced error handling -- [ ] Comprehensive testing (80%+ coverage) - -### πŸ“… Phase 3: Intelligence (Q1 2026) -- [ ] Post-publication analytics -- [ ] ML-based optimal timing -- [ ] Sentiment analysis -- [ ] A/B testing framework -- [ ] Trending hashtag detection -- [ ] Multi-language support - -### 🎨 Phase 4: Admin Dashboard (Q2 2026) -- [ ] Web based UI -- [ ] Real-time analytics -- [ ] Manual post scheduling -- [ ] Content calendar -- [ ] Performance metrics -- [ ] Mobile app (MAUI) - -### 🌍 Phase 5: Expansion (Q3 2026) -- [ ] Instagram publishing (complete setup) -- [ ] Threads (Meta) integration -- [ ] Mastodon support -- [ ] BlueSky protocol -- [ ] YouTube Shorts -- [ ] Podcast automation - ---- - -## Contributing - -Contributions, issues, and feature requests are welcome! - -### How to Contribute - -1. **Fork** the project -2. **Create** your feature branch (`git checkout -b feature/AmazingFeature`) -3. **Commit** your changes (`git commit -m 'Add some AmazingFeature'`) -4. **Push** to the branch (`git push origin feature/AmazingFeature`) -5. **Open** a Pull Request - -### Guidelines - -- Follow C# (.NET) coding conventions -- Add unit tests for new features -- Update documentation -- Keep commits atomic and descriptive -- Respect existing design patterns - -### Coding Standards - -```csharp -// βœ… Good -public async Task GenerateAsync() -{ - var summary = await _aiService.GetSummaryAsync(content, maxLength); - if (string.IsNullOrWhiteSpace(summary)) - { - _logger.LogWarning("Empty summary generated"); - return null; - } - return new Post { Content = summary }; -} - -// ❌ Avoid -public async Task GenerateAsync() { - var summary = await _aiService.GetSummaryAsync(content, maxLength); - if (summary == null || summary == "") return null; - return new Post { Content = summary }; -} -``` - ---- - -## License - -This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. - -``` -MIT License - -Copyright (c) 2025 Marco Cavallo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -``` - - ---- - -## Author - -**Marco Cavallo** - -- 🌐 Website: [xposter.artcava.net](https://xposter.artcava.net) -- πŸ’Ό LinkedIn: [Marco Cavallo](https://linkedin.com/in/artcava) -- 🐦 Twitter: [@artcava](https://twitter.com/artcava) -- πŸ“§ Email: cavallo.marco@gmail.com -- 🏒 Location: Turin, Italy - ---- - -## Acknowledgments - -- [Azure Functions](https://azure.microsoft.com/services/functions/) - Serverless platform -- [OpenAI](https://openai.com/) - AI models (gpt-4.1-nano, gpt-image-1.5) -- [LinqToTwitter](https://github.com/JoeMayo/LinqToTwitter) - Twitter API wrapper -- [.NET Foundation](https://dotnetfoundation.org/) - Framework and community - ---- - -## Support - -- **Issues**: [GitHub Issues](https://github.com/artcava/XPoster/issues) -- **Discussions**: [GitHub Discussions](https://github.com/artcava/XPoster/discussions) -- **Email**: cavallo.marco@gmail.com - ---- - -## Star History - -If you find this project useful, consider leaving a ⭐ on GitHub! - -[![Star History Chart](https://api.star-history.com/svg?repos=artcava/XPoster&type=Date)](https://star-history.com/#artcava/XPoster&Date) - ---- - -
- -**Made with ❀️ in Turin, Italy** - -[🏠 Homepage](https://xposter.artcava.net/) β€’ -[πŸ“– Documentation](docs/index.md) β€’ -[πŸ› Report Bug](https://github.com/artcava/XPoster/issues) β€’ -[πŸ’‘ Request Feature](https://github.com/artcava/XPoster/issues) - -
+IyBYUG9zdGVyIPCfmoAKCls \ No newline at end of file From bf11b266b3b841c9fc109eb0647cd839feda23d1 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Fri, 10 Apr 2026 16:28:09 +0200 Subject: [PATCH 2/3] fix: update Build and Deploy badge to point to ci.yml The workflow was renamed from master_xposterfunction.yml to ci.yml. The badge URL in README.md was never updated, causing it to render as broken. Also updated the reference in the Deployment section. Fixes #82. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb2cd80..cf724bb 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -IyBYUG9zdGVyIPCfmoAKCls \ No newline at end of file +IyBYUG9zdGVyIPCfmoAKClshW0F6dXJlIEZ1bmN0aW9uc10oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9BenVyZSUyMEZ1bmN0aW9ucy12NC0wMDYyQUQ/bG9nbz1henVyZWZ1bmN0aW9ucyZsb2dvQ29sb3I9d2hpdGUpXShodHRwczovL2F6dXJlLm1pY3Jvc29mdC5jb20vZW4tdXMvc2VydmljZXMvZnVuY3Rpb25zLykKWyFbLk5FVF0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS8uTkVULTguMC01MTJCRDQ/bG9nbz1kb3RuZXQmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9kb3RuZXQubWljcm9zb2Z0LmNvbS8pClshW0MjXShodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL0MlMjMtMTIuMC0yMzkxMjA/bG9nbz1jc2hhcnAmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9kb2NzLm1pY3Jvc29mdC5jb20vZW4tdXMvZG90bmV0L2NzaGFycC8pClshW09wZW5BSV0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9PcGVuQUktUG93ZXJlZC00MTI5OTE/bG9nbz1vcGVuYWkmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9vcGVuYWkuY29tLykKWyFbTGljZW5zZV0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9MaWNlbnNlLU1JVC15ZWxsb3cuc3ZnKV0oTElDRU5TRSkKWyFbRGVwbG95bWVudF0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9EZXBsb3llZC1BenVyZS1ibHVlKV0oaHR0cHM6Ly94cG9zdGVyZnVuY3Rpb24uYXp1cmV3ZWJzaXRlcy5uZXQvKQpbIVtCdWlsZCBhbmQgRGVwbG95XShodHRwczovL2dpdGh1Yi5jb20vYXJ0Y2F2YS9YUG9zdGVyL2FjdGlvbnMvd29ya2Zsb3dzL2NpLnltbC9iYWRnZS5zdmcpXShodHRwczovL2dpdGh1Yi5jb20vYXJ0Y2F2YS9YUG9zdGVyL2FjdGlvbnMvd29ya2Zsb3dzL2NpLnltbCkKCj4gKipBSS1Qb3dlcmVkIFNvY2lhbCBNZWRpYSBBdXRvbWF0aW9uIFBsYXRmb3JtKioKPiAKPiBYUG9zdGVyIGlzIGFuIEF6dXJlIEZ1bmN0aW9uIHRoYXQgYXV0b21hdGVzIGNvbnRlbnQgcHVibGlzaGluZyBhY3Jvc3MgbXVsdGlwbGUgc29jaWFsIG1lZGlhIHBsYXRmb3JtcyAoVHdpdHRlci9YLCBMaW5rZWRJbiwgSW5zdGFncmFtKSB1c2luZyBhcnRpZmljaWFsIGludGVsbGlnZW5jZSBmb3IgY29udGVudCBnZW5lcmF0aW9uIGFuZCBjdXJhdGlvbi4KCi0tLQoKIyMg8J+TiyBUYWJsZSBvZiBDb250ZW50cwoKLSBbRmVhdHVyZXNdKCNmZWF0dXJlcykKLSBbQXJjaGl0ZWN0dXJlXSgjYXJjaGl0ZWN0dXJlKQotIFtUZWNobm9sb2dpZXNdKCN0ZWNobm9sb2dpZXMpCi0gW0dldHRpbmcgU3RhcnRlZF0oI2dldHRpbmctc3RhcnRlZCkKLSBbQ29uZmlndXJhdGlvbl0oI2NvbmZpZ3VyYXRpb24pCi0gW0RlcGxveW1lbnRdKCNkZXBsb3ltZW50KQotIFtVc2FnZV0oI3VzYWdlKQotIFtTY2hlZHVsaW5nXSgjc2NoZWR1bGluZykKLSBbRXh0ZW5zaWJpbGl0eV0oI2V4dGVuc2liaWxpdHkpCi0gW1Rlc3RpbmddKCN0ZXN0aW5nKQotIFtNb25pdG9yaW5nXSgjbW9uaXRvcmluZykKLSBbUm9hZG1hcF0oI3JvYWRtYXApCi0gW0NvbnRyaWJ1dGluZ10oI2NvbnRyaWJ1dGluZykKLSBbTGljZW5zZV0oI2xpY2Vuc2UpCgo+IPCfk JGIGZvciBhIGRlZXAtZGl2ZSBpbnRvIGFyY2hpdGVjdHVyYWwgZGVjaXNpb25zLCBkZXNpZ24gcGF0dGVybnMsIEFEUnMsIGFuZCBleHRlbnNpb24gY29udHJhY3RzLCBzZWUgW0FSQ0hJVEVDVFVSRS5tZF0oQVJDSElURUNUVVJFLm1kKS4KCi0tLQoKIyMgRmVhdHVyZXMKCiMjIyDwn6S/IENvbnRlbnQgR2VuZXJhdGlvbgotICoqQUktUG93ZXJlZCBTdW1tYXJpemF0aW9uKio6IEludGVsbGlnZW50IFJTUyBmZWVkIHN1bW1hcmllcyB1c2luZyBncHQtNC4xLW5hbm8KLSAqKkltYWdlIEdlbmVyYXRpb24qKjogQXV0b21hdGljIGNvbnRleHR1YWwgaW1hZ2UgY3JlYXRpb24gd2l0aCBncHQtaW1hZ2UtMS41Ci0gKipTbWFydCBIYXNodGFncyoqOiBBdXRvbWF0aWMga2V5d29yZCBjb252ZXJzaW9uIHRvIG9wdGltaXplZCBoYXNodGFncwotICoqTXVsdGktU3RyYXRlZ3kqKjogU3VwcG9ydCBmb3IgZGlmZmVyZW50IGNvbnRlbnQgZ2VuZXJhdGlvbiBhbGdvcml0aG1zCgojIyMg8J+MkCBNdWx0aS1QbGF0Zm9ybSBQdWJsaXNoaW5nCi0gKipUd2l0dGVyL1gqKjogQXV0b21hdGVkIHBvc3Rpbmcgd2l0aCBpbWFnZSBzdXBwb3J0Ci0gKipMaW5rZWRJbioqOiBQb3N0cyB0byBhIHBlcnNvbmFsIExpbmtlZEluIHByb2ZpbGUgdmlhIHRoZSBVR0MgUG9zdHMgQVBJLiBDb21wYW55IHBhZ2Ugc3VwcG9ydCBpcyBwbGFubmVkIChzZWUgaXNzdWUgI1hYKS4KLSAqKkluc3RhZ3JhbSoqOiBQdWJsaXNoaW5nIHZpYSBHcmFwaCBBUEkgKGluIGRldmVsb3BtZW50LCBzZWUgaXNzdWUgI1hYIGZvciBwcm9kdWN0aW9uIHJlYWRpbmVzcyBjaGVja2xpc3QpCgojIyMg4pif77iPIEF1dG9tYXRpb24gJiBTY2hlZHVsaW5nCi0gKipUaW1lci1CYXNlZCBFeGVjdXRpb24qKjogQ29uZmlndXJhYmxlIGF1dG9tYXRpYyBleGVjdXRpb24KLSAqKlNtYXJ0IFNjaGVkdWxpbmcqKjogRGlmZmVyZW50IHBvc3Rpbmcgc3RyYXRlZ2llcyBiYXNlZCBvbiB0aW1lCi0gKipDb25kaXRpb25hbCBMb2dpYyoqOiBQdWJsaXNoaW5nIG9ubHkgd2hlbiBhcHByb3ByaWF0ZQotICoqRmxleGlibGUgQ29uZmlndXJhdGlvbioqOiBDdXN0b21pemFibGUgc2NoZWR1bGUgdmlhIGVudmlyb25tZW50IHZhcmlhYmxlcwoKIyMjIPCfk4cgRW50ZXJwcmlzZSBGZWF0dXJlcwotICoqQXBwbGljYXRpb24gSW5zaWdodHMqKjogQ29tcGxldGUgbW9uaXRvcmluZyBhbmQgdGVsZW1ldHJ5Ci0gKipTdHJ1Y3R1cmVkIExvZ2dpbmcqKjogRGV0YWlsZWQgbG9ncyBmb3IgZGVidWdnaW5nIGFuZCBhdWRpdAotICoqRXJyb3IgSGFuZGxpbmcqKjogUm9idXN0IGVycm9yIG1hbmFnZW1lbnQgd2l0aCByZXRyeSBsb2dpYwotICoqRGVwZW5kZW5jeSBJbmplY3Rpb24qKjogTW9kdWxhciBhbmQgdGVzdGFibGUgYXJjaGl0ZWN0dXJlCgotLS0KCiMjIEFyY2hpdGVjdHVyZQoKPiDwn5SRIGZ vciBUaGUgZnVsbCBhcmNoaXRlY3R1cmFsIHJhdGlvbmFsZSwgQURScywgZGVzaWduIHBhdHRlcm5zLCBhbmQgTWVybWFpZCBkYXRhLWZsb3cgZGlhZ3JhbSwgc2VlICoqW0FSQ0hJVEVDVFVSRS5tZF0oQVJDSElURUNUVVJFLm1kKSoqLgoKIyMjIEhpZ2gtTGV2ZWwgT3ZlcnZpZXcKCmBgYAriiJzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAK4pSCICAgQXp1cmUgVGltZXIgVHJpZ2dlciAgICAgIOKUggrilIIgICAoY29uZmlndXJhYmxlIHNjaGVkdWxlKSAg4pSCCuKUlOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUguKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmAogICAgICAgICAgICB8CiAgICAgICAgICAgIOKUrAriiaziio7ilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAK4pSCICAgR2VuZXJhdG9yIEZhY3RvcnkgICAgICAgIOKUgiDihpAgLS0tLSBTdHJhdGVneSBQYXR0ZXJuCuKUgiAgIChUaW1lLWJhc2VkIFNlbGVjdG9yKSAgICDilIIK4pSU4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSC4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYCiAgICAgICAgICAgIHwKICAgICDilJDilIDilIDilIDilIDilIDilIDilIziio4gICAgICAgICDilJDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIwK4pSCICAgICAgICAgICAg4pSCICAgICAgICAgICAg4pSCCuKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmCAgIOKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmCAgIOKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmArilIIgICBGZWVkICAgfCAgIHwgUG93ZXJMYXcgfCAgIHwgICAgTm8gICAgfArilIIgR2VuZXJhdG9yIHwgICB8IEdlbmVyYXRvciB8ICAgfCBHZW5lcmF0b3IgfArilJTilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilJTilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJgKICAgICAgfCAgICAgICAgICAgICAgfAogICAgICDilJDilIDilIDilIDilIDilIDilIDilJDilIDilIDilIDilIDilJgKICAgICAgICAgICAgIHwKICAgICAgICAgICAgIOKUrAogICAg4pSM4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICB84pSCICAgU2VydmljZXMgICAgIHwKICAgIHzilIzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJAKICAgIHzilIIg4oCiIEFJIFNlcnZpY2UgICB8IOKGkCAtLS0gT3BlbkFJIEludGVncmF0aW9uCiAgICB84pSCIOKAoiBGZWVkIFNlcnZpY2UgfCDihpAgLS0tIFJTUyBQYXJzZXIKICAgIHzilIIg4oCiIENyeXB0byBTdmMgICB8IOKGkCAtLS0gU2VjdXJpdHkgVXRpbHMKICAgIHzilJTilIDilIDilIDilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilIDilIDilJgKICAgICAgICAgICAgIHwKICAgICAgICAgICAgIOKUrAogICAg4pSM4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICB84pSCIFNlbmRlciBQbHVnaW5zIHwKICAgIHzilIzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJAKICAgIHzilIIg4oCiIFhTZW5kZXIgICAgICB8IOKGkCAtLS0gVHdpdHRlci9YIEFQSQ== \ No newline at end of file From 0019ad5ad9c05aeba01bb705f765795b6de1eea2 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Fri, 10 Apr 2026 17:03:21 +0200 Subject: [PATCH 3/3] fix: update Build and Deploy badge workflow path Badge was pointing to master_xposterfunction.yml which no longer exists. Updated reference to ci.yml, the current CI/CD workflow. --- README.md | 948 +++++++++++++++++++++++++++++++++++++++++++++++++++- XPoster.sln | 22 ++ 2 files changed, 969 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf724bb..7330f4a 100644 --- a/README.md +++ b/README.md @@ -1 +1,947 @@ -IyBYUG9zdGVyIPCfmoAKClshW0F6dXJlIEZ1bmN0aW9uc10oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9BenVyZSUyMEZ1bmN0aW9ucy12NC0wMDYyQUQ/bG9nbz1henVyZWZ1bmN0aW9ucyZsb2dvQ29sb3I9d2hpdGUpXShodHRwczovL2F6dXJlLm1pY3Jvc29mdC5jb20vZW4tdXMvc2VydmljZXMvZnVuY3Rpb25zLykKWyFbLk5FVF0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS8uTkVULTguMC01MTJCRDQ/bG9nbz1kb3RuZXQmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9kb3RuZXQubWljcm9zb2Z0LmNvbS8pClshW0MjXShodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL0MlMjMtMTIuMC0yMzkxMjA/bG9nbz1jc2hhcnAmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9kb2NzLm1pY3Jvc29mdC5jb20vZW4tdXMvZG90bmV0L2NzaGFycC8pClshW09wZW5BSV0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9PcGVuQUktUG93ZXJlZC00MTI5OTE/bG9nbz1vcGVuYWkmbG9nb0NvbG9yPXdoaXRlKV0oaHR0cHM6Ly9vcGVuYWkuY29tLykKWyFbTGljZW5zZV0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9MaWNlbnNlLU1JVC15ZWxsb3cuc3ZnKV0oTElDRU5TRSkKWyFbRGVwbG95bWVudF0oaHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9EZXBsb3llZC1BenVyZS1ibHVlKV0oaHR0cHM6Ly94cG9zdGVyZnVuY3Rpb24uYXp1cmV3ZWJzaXRlcy5uZXQvKQpbIVtCdWlsZCBhbmQgRGVwbG95XShodHRwczovL2dpdGh1Yi5jb20vYXJ0Y2F2YS9YUG9zdGVyL2FjdGlvbnMvd29ya2Zsb3dzL2NpLnltbC9iYWRnZS5zdmcpXShodHRwczovL2dpdGh1Yi5jb20vYXJ0Y2F2YS9YUG9zdGVyL2FjdGlvbnMvd29ya2Zsb3dzL2NpLnltbCkKCj4gKipBSS1Qb3dlcmVkIFNvY2lhbCBNZWRpYSBBdXRvbWF0aW9uIFBsYXRmb3JtKioKPiAKPiBYUG9zdGVyIGlzIGFuIEF6dXJlIEZ1bmN0aW9uIHRoYXQgYXV0b21hdGVzIGNvbnRlbnQgcHVibGlzaGluZyBhY3Jvc3MgbXVsdGlwbGUgc29jaWFsIG1lZGlhIHBsYXRmb3JtcyAoVHdpdHRlci9YLCBMaW5rZWRJbiwgSW5zdGFncmFtKSB1c2luZyBhcnRpZmljaWFsIGludGVsbGlnZW5jZSBmb3IgY29udGVudCBnZW5lcmF0aW9uIGFuZCBjdXJhdGlvbi4KCi0tLQoKIyMg8J+TiyBUYWJsZSBvZiBDb250ZW50cwoKLSBbRmVhdHVyZXNdKCNmZWF0dXJlcykKLSBbQXJjaGl0ZWN0dXJlXSgjYXJjaGl0ZWN0dXJlKQotIFtUZWNobm9sb2dpZXNdKCN0ZWNobm9sb2dpZXMpCi0gW0dldHRpbmcgU3RhcnRlZF0oI2dldHRpbmctc3RhcnRlZCkKLSBbQ29uZmlndXJhdGlvbl0oI2NvbmZpZ3VyYXRpb24pCi0gW0RlcGxveW1lbnRdKCNkZXBsb3ltZW50KQotIFtVc2FnZV0oI3VzYWdlKQotIFtTY2hlZHVsaW5nXSgjc2NoZWR1bGluZykKLSBbRXh0ZW5zaWJpbGl0eV0oI2V4dGVuc2liaWxpdHkpCi0gW1Rlc3RpbmddKCN0ZXN0aW5nKQotIFtNb25pdG9yaW5nXSgjbW9uaXRvcmluZykKLSBbUm9hZG1hcF0oI3JvYWRtYXApCi0gW0NvbnRyaWJ1dGluZ10oI2NvbnRyaWJ1dGluZykKLSBbTGljZW5zZV0oI2xpY2Vuc2UpCgo+IPCfk JGIGZvciBhIGRlZXAtZGl2ZSBpbnRvIGFyY2hpdGVjdHVyYWwgZGVjaXNpb25zLCBkZXNpZ24gcGF0dGVybnMsIEFEUnMsIGFuZCBleHRlbnNpb24gY29udHJhY3RzLCBzZWUgW0FSQ0hJVEVDVFVSRS5tZF0oQVJDSElURUNUVVJFLm1kKS4KCi0tLQoKIyMgRmVhdHVyZXMKCiMjIyDwn6S/IENvbnRlbnQgR2VuZXJhdGlvbgotICoqQUktUG93ZXJlZCBTdW1tYXJpemF0aW9uKio6IEludGVsbGlnZW50IFJTUyBmZWVkIHN1bW1hcmllcyB1c2luZyBncHQtNC4xLW5hbm8KLSAqKkltYWdlIEdlbmVyYXRpb24qKjogQXV0b21hdGljIGNvbnRleHR1YWwgaW1hZ2UgY3JlYXRpb24gd2l0aCBncHQtaW1hZ2UtMS41Ci0gKipTbWFydCBIYXNodGFncyoqOiBBdXRvbWF0aWMga2V5d29yZCBjb252ZXJzaW9uIHRvIG9wdGltaXplZCBoYXNodGFncwotICoqTXVsdGktU3RyYXRlZ3kqKjogU3VwcG9ydCBmb3IgZGlmZmVyZW50IGNvbnRlbnQgZ2VuZXJhdGlvbiBhbGdvcml0aG1zCgojIyMg8J+MkCBNdWx0aS1QbGF0Zm9ybSBQdWJsaXNoaW5nCi0gKipUd2l0dGVyL1gqKjogQXV0b21hdGVkIHBvc3Rpbmcgd2l0aCBpbWFnZSBzdXBwb3J0Ci0gKipMaW5rZWRJbioqOiBQb3N0cyB0byBhIHBlcnNvbmFsIExpbmtlZEluIHByb2ZpbGUgdmlhIHRoZSBVR0MgUG9zdHMgQVBJLiBDb21wYW55IHBhZ2Ugc3VwcG9ydCBpcyBwbGFubmVkIChzZWUgaXNzdWUgI1hYKS4KLSAqKkluc3RhZ3JhbSoqOiBQdWJsaXNoaW5nIHZpYSBHcmFwaCBBUEkgKGluIGRldmVsb3BtZW50LCBzZWUgaXNzdWUgI1hYIGZvciBwcm9kdWN0aW9uIHJlYWRpbmVzcyBjaGVja2xpc3QpCgojIyMg4pif77iPIEF1dG9tYXRpb24gJiBTY2hlZHVsaW5nCi0gKipUaW1lci1CYXNlZCBFeGVjdXRpb24qKjogQ29uZmlndXJhYmxlIGF1dG9tYXRpYyBleGVjdXRpb24KLSAqKlNtYXJ0IFNjaGVkdWxpbmcqKjogRGlmZmVyZW50IHBvc3Rpbmcgc3RyYXRlZ2llcyBiYXNlZCBvbiB0aW1lCi0gKipDb25kaXRpb25hbCBMb2dpYyoqOiBQdWJsaXNoaW5nIG9ubHkgd2hlbiBhcHByb3ByaWF0ZQotICoqRmxleGlibGUgQ29uZmlndXJhdGlvbioqOiBDdXN0b21pemFibGUgc2NoZWR1bGUgdmlhIGVudmlyb25tZW50IHZhcmlhYmxlcwoKIyMjIPCfk4cgRW50ZXJwcmlzZSBGZWF0dXJlcwotICoqQXBwbGljYXRpb24gSW5zaWdodHMqKjogQ29tcGxldGUgbW9uaXRvcmluZyBhbmQgdGVsZW1ldHJ5Ci0gKipTdHJ1Y3R1cmVkIExvZ2dpbmcqKjogRGV0YWlsZWQgbG9ncyBmb3IgZGVidWdnaW5nIGFuZCBhdWRpdAotICoqRXJyb3IgSGFuZGxpbmcqKjogUm9idXN0IGVycm9yIG1hbmFnZW1lbnQgd2l0aCByZXRyeSBsb2dpYwotICoqRGVwZW5kZW5jeSBJbmplY3Rpb24qKjogTW9kdWxhciBhbmQgdGVzdGFibGUgYXJjaGl0ZWN0dXJlCgotLS0KCiMjIEFyY2hpdGVjdHVyZQoKPiDwn5SRIGZ vciBUaGUgZnVsbCBhcmNoaXRlY3R1cmFsIHJhdGlvbmFsZSwgQURScywgZGVzaWduIHBhdHRlcm5zLCBhbmQgTWVybWFpZCBkYXRhLWZsb3cgZGlhZ3JhbSwgc2VlICoqW0FSQ0hJVEVDVFVSRS5tZF0oQVJDSElURUNUVVJFLm1kKSoqLgoKIyMjIEhpZ2gtTGV2ZWwgT3ZlcnZpZXcKCmBgYAriiJzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAK4pSCICAgQXp1cmUgVGltZXIgVHJpZ2dlciAgICAgIOKUggrilIIgICAoY29uZmlndXJhYmxlIHNjaGVkdWxlKSAg4pSCCuKUlOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUguKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmAogICAgICAgICAgICB8CiAgICAgICAgICAgIOKUrAriiaziio7ilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAK4pSCICAgR2VuZXJhdG9yIEZhY3RvcnkgICAgICAgIOKUgiDihpAgLS0tLSBTdHJhdGVneSBQYXR0ZXJuCuKUgiAgIChUaW1lLWJhc2VkIFNlbGVjdG9yKSAgICDilIIK4pSU4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSC4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYCiAgICAgICAgICAgIHwKICAgICDilJDilIDilIDilIDilIDilIDilIDilIziio4gICAgICAgICDilJDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIwK4pSCICAgICAgICAgICAg4pSCICAgICAgICAgICAg4pSCCuKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmCAgIOKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmCAgIOKUjOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUmArilIIgICBGZWVkICAgfCAgIHwgUG93ZXJMYXcgfCAgIHwgICAgTm8gICAgfArilIIgR2VuZXJhdG9yIHwgICB8IEdlbmVyYXRvciB8ICAgfCBHZW5lcmF0b3IgfArilJTilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilJTilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJgKICAgICAgfCAgICAgICAgICAgICAgfAogICAgICDilJDilIDilIDilIDilIDilIDilIDilJDilIDilIDilIDilIDilJgKICAgICAgICAgICAgIHwKICAgICAgICAgICAgIOKUrAogICAg4pSM4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICB84pSCICAgU2VydmljZXMgICAgIHwKICAgIHzilIzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJAKICAgIHzilIIg4oCiIEFJIFNlcnZpY2UgICB8IOKGkCAtLS0gT3BlbkFJIEludGVncmF0aW9uCiAgICB84pSCIOKAoiBGZWVkIFNlcnZpY2UgfCDihpAgLS0tIFJTUyBQYXJzZXIKICAgIHzilIIg4oCiIENyeXB0byBTdmMgICB8IOKGkCAtLS0gU2VjdXJpdHkgVXRpbHMKICAgIHzilJTilIDilIDilIDilIDilIDilIDilIDilIDilILilIDilIDilIDilIDilIDilIDilIDilJgKICAgICAgICAgICAgIHwKICAgICAgICAgICAgIOKUrAogICAg4pSM4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICB84pSCIFNlbmRlciBQbHVnaW5zIHwKICAgIHzilIzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilJAKICAgIHzilIIg4oCiIFhTZW5kZXIgICAgICB8IOKGkCAtLS0gVHdpdHRlci9YIEFQSQ== \ No newline at end of file +# XPoster πŸš€ + +[![Azure Functions](https://img.shields.io/badge/Azure%20Functions-v4-0062AD?logo=azurefunctions&logoColor=white)](https://azure.microsoft.com/en-us/services/functions/) +[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) +[![C#](https://img.shields.io/badge/C%23-12.0-239120?logo=csharp&logoColor=white)](https://docs.microsoft.com/en-us/dotnet/csharp/) +[![OpenAI](https://img.shields.io/badge/OpenAI-Powered-412991?logo=openai&logoColor=white)](https://openai.com/) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Deployment](https://img.shields.io/badge/Deployed-Azure-blue)](https://xposterfunction.azurewebsites.net/) +[![Build and Deploy](https://github.com/artcava/XPoster/actions/workflows/ci.yml/badge.svg)](https://github.com/artcava/XPoster/actions/workflows/ci.yml) + +> **AI-Powered Social Media Automation Platform** +> +> XPoster is an Azure Function that automates content publishing across multiple social media platforms (Twitter/X, LinkedIn, Instagram) using artificial intelligence for content generation and curation. + +--- + +## πŸ“‹ Table of Contents + +- [Features](#features) +- [Architecture](#architecture) +- [Technologies](#technologies) +- [Getting Started](#getting-started) +- [Configuration](#configuration) +- [Deployment](#deployment) +- [Usage](#usage) +- [Scheduling](#scheduling) +- [Extensibility](#extensibility) +- [Testing](#testing) +- [Monitoring](#monitoring) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) + +> πŸ“ For a deep-dive into architectural decisions, design patterns, ADRs, and extension contracts, see [ARCHITECTURE.md](ARCHITECTURE.md). + +--- + +## Features + +### πŸ€– Content Generation +- **AI-Powered Summarization**: Intelligent RSS feed summaries using gpt-4.1-nano +- **Image Generation**: Automatic contextual image creation with gpt-image-1.5 +- **Smart Hashtags**: Automatic keyword conversion to optimized hashtags +- **Multi-Strategy**: Support for different content generation algorithms + +### 🌐 Multi-Platform Publishing +- **Twitter/X**: Automated posting with image support +- **LinkedIn**: Posts on personal profiles and company pages +- **Instagram**: Publishing via Graph API (in development) + +### βš™οΈ Automation & Scheduling +- **Timer-Based Execution**: Configurable automatic execution +- **Smart Scheduling**: Different posting strategies based on time +- **Conditional Logic**: Publishing only when appropriate +- **Flexible Configuration**: Customizable schedule via environment variables + +### πŸ“Š Enterprise Features +- **Application Insights**: Complete monitoring and telemetry +- **Structured Logging**: Detailed logs for debugging and audit +- **Error Handling**: Robust error management with retry logic +- **Dependency Injection**: Modular and testable architecture + +--- + +## Architecture + +> πŸ“ For the full architectural rationale, ADRs, design patterns, and Mermaid data-flow diagram, see **[ARCHITECTURE.md](ARCHITECTURE.md)**. + +### High-Level Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Azure Timer Trigger β”‚ +β”‚ (configurable schedule) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Generator Factory β”‚ ◄─── Strategy Pattern +β”‚ (Time-based Selector) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Feed β”‚ β”‚ PowerLaw β”‚ β”‚ No β”‚ +β”‚Generator β”‚ β”‚Generator β”‚ β”‚Generator β”‚ +β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Services β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ β€’ AI Service β”‚ ◄─── OpenAI Integration + β”‚ β€’ Feed Service β”‚ ◄─── RSS Parser + β”‚ β€’ Crypto Svc β”‚ ◄─── Security Utils + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Sender Plugins β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ β€’ XSender β”‚ ◄─── Twitter/X API + β”‚ β€’ InSender β”‚ ◄─── LinkedIn API + β”‚ β€’ IgSender β”‚ ◄─── Instagram API + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Core Components + +#### 1. **XFunction** (Entry Point) +Timer-triggered Azure Function that orchestrates the entire publishing workflow. + +**Cron Expression**: Configurable via environment variable (default: `0 5 * * * *`) + +#### 2. **GeneratorFactory** (Factory + Strategy Pattern) +Dynamically selects the appropriate generator based on current time. + +| Time | Platform | Strategy | +|------|----------|----------| +| 06:00 | LinkedIn | Feed Summary | +| 08:00 | Twitter/X | Feed Summary | +| 14:00 | LinkedIn | Power Law | +| 16:00 | Twitter/X | Power Law | + +#### 3. **Generators** (Content Strategy) +- **FeedGenerator**: Analyzes crypto RSS feeds, generates AI summaries, creates images +- **PowerLawGenerator**: Generates content based on statistical distribution +- **NoGenerator**: Placeholder for time slots without publishing + +#### 4. **Services Layer** +- **AiService**: Interface with OpenAI (gpt-4.1-nano, gpt-image-1.5) +- **FeedService**: RSS parser with caching and intelligent filtering +- **CryptoService**: Crypto-currencies utilities + +#### 5. **Sender Plugins** (Platform Abstraction) +- **XSender**: Twitter/X via LinqToTwitter +- **InSender**: LinkedIn via HTTP API +- **IgSender**: Instagram via Graph API (in development) + +--- + +## Technologies + +### Core Framework +- **.NET 8.0** - Main framework +- **Azure Functions v4** - Serverless compute +- **C# 12** - Programming language + +### AI & ML +- **OpenAI** - gpt-4.1-nano for summarization +- **gpt-image-1.5** - Image generation + +### Social Media APIs +- **LinqToTwitter 6.15.0** - Twitter/X integration +- **LinkedIn REST API v2** - LinkedIn publishing +- **Instagram Graph API** - Instagram (in development) + +### Monitoring & Logging +- **Application Insights** - Telemetry and monitoring +- **ILogger** - Structured logging + +### Utilities +- **System.ServiceModel.Syndication** - RSS parsing +- **Microsoft.Extensions.Http** - HTTP client factory + +--- + +## Getting Started + +### Prerequisites + +- **.NET 8.0 SDK** ([Download](https://dotnet.microsoft.com/download/dotnet/8.0)) +- **Azure Functions Core Tools** ([Install](https://docs.microsoft.com/azure/azure-functions/functions-run-local)) +- **Visual Studio 2022** or **Visual Studio Code** +- **Azure Account** (with active subscription) +- **OpenAI API** (with gpt-4.1-nano and gpt-image-1.5 enabled) + +### Clone the Repository + +```bash +git clone https://github.com/artcava/XPoster.git +cd XPoster +``` + +### Restore Dependencies + +```bash +dotnet restore +``` + +### Build the Project + +```bash +dotnet build +``` + +### Run Tests + +```bash +dotnet test +``` + +### Configure Local Settings + +A template file with all required keys and inline documentation is versioned at [`src/local.settings.json.example`](src/local.settings.json.example). + +Copy it and fill in your credentials before running the function locally: + +```bash +cp src/local.settings.json.example src/local.settings.json +``` + +Then open `src/local.settings.json` and replace every empty string `""` with the actual value for each service. See the [Configuration](#configuration) section for details on where to obtain each credential. + +> ⚠️ `local.settings.json` is listed in `.gitignore` and will **never** be committed. The `.example` variant is safe to version because it contains no real secrets. + +> πŸ“– For the full expanded setup guide with troubleshooting tips, see [docs/getting-started.md](docs/getting-started.md). + +--- + +## Configuration + +### 1. Local Development + +Create a `local.settings.json` file in the `src/` directory: + +```json +{ + "IsEncrypted": false, + "Values": { + "CronSchedule": "0 5 * * * *", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + + "X_API_KEY": "your_twitter_api_key", + "X_API_SECRET": "your_twitter_api_secret", + "X_ACCESS_TOKEN": "your_twitter_access_token", + "X_ACCESS_TOKEN_SECRET": "your_twitter_access_token_secret", + + "LINKEDIN_ACCESS_TOKEN": "your_linkedin_token", + "LINKEDIN_ORGANIZATION_ID": "your_linkedin_org_id", + + "INSTAGRAM_ACCESS_TOKEN": "your_instagram_token", + "INSTAGRAM_BUSINESS_ACCOUNT_ID": "your_instagram_account_id", + + "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/", + "AZURE_OPENAI_KEY": "your_openai_key", + "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4.1-nano" + } +} +``` + +> πŸ“– Full configuration reference with types, defaults, and where to obtain each credential: [docs/configuration.md](docs/configuration.md). + +### 2. Azure Configuration + +#### App Settings (Azure Portal) + +Navigate to **Azure Portal** β†’ **Function App** β†’ **Configuration** β†’ **Application Settings** + +Add the same variables from `local.settings.json`. + +#### Managed Identity (Recommended) + +For enhanced security, use Azure Managed Identity: + +1. Enable **System Assigned Managed Identity** on the Function App +2. Assign appropriate roles on: + - Azure OpenAI Service + - Azure Key Vault (for secrets) +3. Modify `Program.cs` to use `DefaultAzureCredential` + +```csharp +builder.Services.AddSingleton(sp => +{ + var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")); + return new OpenAIClient(endpoint, new DefaultAzureCredential()); +}); +``` + +--- + +## Deployment + +### Option 1: GitHub Actions (Automated CI/CD) + +The repository includes a GitHub Actions workflow (`.github/workflows/master_xposterfunction.yml`). + +**Setup**: +1. Create a Function App in Azure Portal +2. Download the **Publish Profile** from the Function App +3. Add the content as a **Secret** in GitHub: + - Name: `AZURE_FUNCTIONAPP_PUBLISH_PROFILE` +4. Every push to `master` triggers automatic deployment + +### Option 2: Azure CLI + +```bash +# Login +az login + +# Create Resource Group +az group create --name XPosterRG --location westeurope + +# Create Storage Account +az storage account create \ + --name xposterstorage \ + --resource-group XPosterRG \ + --location westeurope \ + --sku Standard_LRS + +# Create Function App +az functionapp create \ + --name xposterfunction \ + --resource-group XPosterRG \ + --consumption-plan-location westeurope \ + --runtime dotnet-isolated \ + --runtime-version 8 \ + --functions-version 4 \ + --storage-account xposterstorage + +# Deploy +cd src +func azure functionapp publish xposterfunction +``` + +### Option 3: Visual Studio + +1. Right-click on the `XPoster` project +2. Select **Publish** +3. Choose **Azure** β†’ **Azure Function App (Windows)** +4. Select or create a Function App +5. Click **Publish** + +> πŸ“– Step-by-step guide with post-deployment checklist: [docs/deployment.md](docs/deployment.md). + +--- + +## Usage + +### Local Execution + +```bash +cd src +func start +``` + +The function will run locally according to the configured cron expression. + +### Manual Trigger (Azure Portal) + +1. Go to **Azure Portal** β†’ **Function App** β†’ **Functions** +2. Select `XPosterFunction` +3. Click **Test/Run** +4. Click **Run** + +### HTTP Trigger (Optional) + +Add an HTTP trigger for testing: + +```csharp +[Function("XPosterHttpTrigger")] +public async Task RunHttp( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) +{ + await Run(null); + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteStringAsync("XPoster executed successfully"); + return response; +} +``` + +--- + +## Scheduling + +### Schedule Configuration + +The execution frequency is configurable via the `CronSchedule` environment variable: + +**Format**: 6-field cron expression: `{second} {minute} {hour} {day} {month} {dayOfWeek}` + +**Configuration**: + + +```json +//local.settings.json +{ + "Values": { + "CronSchedule": "0 5 * * * *" + } +} +``` + +```bash +//Azure CLI +az functionapp config appsettings set +--name xposterfunction +--resource-group XPosterRG +--settings "CronSchedule=0 5 * * * *" +``` + +### Cron Expression Examples + +| Schedule | Cron Expression | Description | +|----------|-----------------|-------------| +| **Default** | `0 5 */2 * * *` | Every 2 hours at :05 | +| **Hourly** | `0 0 * * * *` | Every hour on the hour | +| **Every 4 hours** | `0 0 */4 * * *` | Every 4 hours | +| **Business Hours** | `0 0 9,12,15,18 * * 1-5` | 9, 12, 15, 18 (Mon-Fri) | +| **Morning/Evening** | `0 0 8,20 * * *` | At 8:00 and 20:00 | +| **Daily** | `0 0 9 * * *` | Every day at 9:00 | +| **Quick Test** | `*/30 * * * * *` | Every 30 seconds (dev only) | + +### Time-based Strategy (GeneratorFactory) + +Modify `GeneratorFactory.cs` to customize which generator to use at each hour: + +```csharp +private static readonly Dictionary sendParameters = new() +{ +{ 6, MessageSender.InSummaryFeed }, // LinkedIn Feed +{ 8, MessageSender.XSummaryFeed }, // Twitter Feed +{ 10, MessageSender.IgSummaryFeed }, // Instagram Feed (enable when ready) +{ 14, MessageSender.InPowerLaw }, // LinkedIn Power Law +{ 16, MessageSender.XPowerLaw }, // Twitter Power Law +{ 18, MessageSender.IgPowerLow }, // Instagram Power Law +}; +``` +--- + +### Best Practices + +βœ… **Testing**: Use frequent schedules in development (`*/5 * * * * *` = every 5 secs) +βœ… **Production**: More conservative schedules to avoid rate limiting +βœ… **Multi-environment**: Different schedules for Dev/Staging/Prod +βœ… **Monitoring**: Check logs to confirm correct execution + +--- + +## Extensibility + +### Adding a New Platform + +**1. Create the Sender Plugin** + +```csharp +// src/SenderPlugins/TikTokSender.cs +public class TikTokSender : ISender +{ + public int MessageMaxLenght => 150; + + public async Task SendAsync(Post post) + { + // Implement TikTok API logic + return true; + } +} +``` + +**2. Register in DI Container** + +```csharp +// src/Program.cs +builder.Services.AddTransient(); +``` + +**3. Add Enum** + +```csharp +// src/Abstraction/Enums.cs +public enum MessageSender +{ + // ... + TikTokSummaryFeed, +} +``` + +**4. Configure Factory** + +```csharp +// src/Implementation/GeneratorFactory.cs +case MessageSender.TikTokSummaryFeed: + return GetInstance( + _serviceProvider.GetService(typeof(TikTokSender)) as ISender + ); +``` + +> πŸ“– Full extension guide with services and design constraints: [docs/extending-xposter.md](docs/extending-xposter.md). + +### Adding a New Generator + +```csharp +// src/Implementation/QuoteGenerator.cs +public class QuoteGenerator : BaseGenerator +{ + public override async Task? GenerateAsync() + { + // Logic to generate motivational quotes + var quote = await _aiService.GetQuoteAsync(); + return new Post { Content = quote }; + } +} +``` + +--- + +## Testing + +### Test Structure + +``` +tests/ +β”œβ”€β”€ XPoster.Tests/ +β”‚ β”œβ”€β”€ Generators/ +β”‚ β”‚ β”œβ”€β”€ FeedGeneratorTests.cs +β”‚ β”‚ └── PowerLawGeneratorTests.cs +β”‚ β”œβ”€β”€ Services/ +β”‚ β”‚ β”œβ”€β”€ AiServiceTests.cs +β”‚ β”‚ └── FeedServiceTests.cs +β”‚ └── SenderPlugins/ +β”‚ β”œβ”€β”€ XSenderTests.cs +β”‚ └── InSenderTests.cs +``` + +### Running Tests + +```bash +# All tests +dotnet test + +# Specific tests +dotnet test --filter "FullyQualifiedName~FeedGenerator" + +# With coverage +dotnet test --collect:"XPlat Code Coverage" +``` + +> πŸ“– Full testing strategy, mocking patterns, and coverage goals: [tests/README.md](tests/README.md). + +### Mocking External Services + +```csharp +[Fact] +public async Task FeedGenerator_ShouldGenerateSummary() +{ + // Arrange + var mockAiService = new Mock(); + mockAiService + .Setup(x => x.GetSummaryAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("Test summary"); + + var generator = new FeedGenerator( + mockSender.Object, + mockLogger.Object, + mockFeedService.Object, + mockAiService.Object + ); + + // Act + var result = await generator.GenerateAsync(); + + // Assert + Assert.NotNull(result); + Assert.Contains("Test summary", result.Content); +} +``` + +--- + +## Monitoring + +### Application Insights Setup + +#### 1. Create the Application Insights Resource + +1. In the **Azure Portal**, search for **Application Insights** and click **Create** +2. Fill in the details: + - **Name**: e.g. `xposter-appinsights` + - **Resource Group**: same as your Function App (`XPosterRG`) + - **Region**: same region as the Function App + - **Resource Mode**: Workspace-based (recommended) +3. Click **Review + Create**, then **Create** +4. Once created, navigate to the resource and copy the **Connection String** (shown on the Overview blade) + +#### 2. Link Application Insights to the Function App + +Add the connection string as an **Application Setting** in the Function App: + +**Via Azure Portal**: +1. Go to **Function App** β†’ **Configuration** β†’ **Application Settings** +2. Click **+ New application setting** +3. Name: `APPLICATIONINSIGHTS_CONNECTION_STRING` +4. Value: paste the full connection string copied above +5. Click **Save** and confirm the restart + +**Via Azure CLI**: +```bash +az functionapp config appsettings set \ + --name xposterfunction \ + --resource-group XPosterRG \ + --settings "APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=;IngestionEndpoint=https://.in.applicationinsights.azure.com/" +``` + +#### 3. SDK Wiring in Program.cs + +The `Microsoft.Azure.Functions.Worker.ApplicationInsights` package is used. It is automatically registered when the connection string is present in the environment. No explicit SDK code is required in `Program.cs` for Azure Functions v4 isolated worker beyond the standard host builder: + +```csharp +// Program.cs β€” Application Insights is enabled automatically +// when APPLICATIONINSIGHTS_CONNECTION_STRING is set. +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => + { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + // ... other registrations + }) + .Build(); +``` + +#### 4. Connection String Configuration + +Add the following key to `local.settings.json` for local telemetry (optional but recommended for debugging): + +```json +{ + "IsEncrypted": false, + "Values": { + "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=;IngestionEndpoint=https://.in.applicationinsights.azure.com/" + } +} +``` + +> ⚠️ The key is already included in [`src/local.settings.json.example`](src/local.settings.json.example). See [#29](https://github.com/artcava/XPoster/issues/29) for the full settings template. + +--- + +### Key Metrics + +- **Execution Count**: Number of function executions +- **Success Rate**: % of successful executions +- **Average Duration**: Average execution time +- **AI Token Usage**: OpenAI token consumption + +--- + +### KQL Queries + +All queries below are verified against the Azure Functions v4 isolated worker table schema (`requests`, `traces`, `dependencies`). + +```kql +// Executions last 24h +requests +| where timestamp > ago(24h) +| where name == "XPosterFunction" +| summarize count() by bin(timestamp, 1h) +| render timechart + +// Error rate (severity >= 3 = Warning+) +traces +| where timestamp > ago(7d) +| where severityLevel >= 3 +| summarize errorCount = count() by bin(timestamp, 1d) +| render barchart + +// AI Cost Tracking +dependencies +| where timestamp > ago(30d) +| where target contains "openai" +| extend tokenUsage = toint(customDimensions.tokenCount) +| summarize totalTokens = sum(tokenUsage), totalCost = sum(tokenUsage) * 0.00006 +``` + +> πŸ’‘ **Tip**: To pin any query result to an Azure Dashboard, run it in the **Logs** blade, click the **Pin to dashboard** icon (πŸ“Œ) in the top-right corner of the results panel, choose your dashboard, and click **Pin**. + +--- + +### Live Metrics (Local Development) + +Application Insights **Live Metrics** streams telemetry in near real-time with sub-second latency β€” useful to verify the function is behaving correctly during local development. + +1. Start the function locally: + ```bash + cd src + func start + ``` +2. In the Azure Portal, open your **Application Insights** resource +3. Click **Live Metrics** in the left-hand menu +4. Trigger a function execution (timer fires automatically, or use an HTTP trigger) +5. Observe incoming requests, dependency calls, exceptions, and custom traces in real time + +> ℹ️ Live Metrics works even in local development as long as `APPLICATIONINSIGHTS_CONNECTION_STRING` is set in `local.settings.json`. + +--- + +### Alerting Configuration + +#### Step-by-Step: Create an Alert via Azure Portal + +The following example creates an alert for **more than 3 consecutive errors within 1 hour**: + +1. In the Azure Portal, navigate to your **Application Insights** resource +2. Select **Alerts** β†’ **+ Create** β†’ **Alert rule** +3. **Scope**: confirm it points to the Application Insights resource +4. **Condition**: + - Click **+ Add condition** + - Signal type: **Custom log search** + - Enter the following KQL query: + ```kql + traces + | where severityLevel >= 3 + | where timestamp > ago(1h) + | summarize errorCount = count() + ``` + - Alert logic: **Greater than** threshold **3** + - Evaluation frequency: `5 minutes` + - Lookback period: `1 hour` +5. **Actions**: + - Click **+ Add action group** β†’ **Create action group** + - Add a notification: type **Email/SMS/Push/Voice**, fill in your email + - Optionally add a **Webhook** action (e.g. to a Slack/Teams incoming webhook URL) +6. **Details**: + - Severity: **2 – Warning** + - Alert rule name: `XPoster - Consecutive Errors` +7. Click **Review + Create** + +#### Recommended Alert Rules + +| Alert | KQL signal | Threshold | Severity | +|-------|-----------|-----------|----------| +| Consecutive errors | `traces \| where severityLevel >= 3` | > 3 in 1h | Sev 2 – Warning | +| Token budget exceeded | `dependencies \| where target contains "openai" \| extend t = toint(customDimensions.tokenCount) \| summarize sum(t)` | > monthly budget | Sev 2 – Warning | +| High latency | `requests \| where name == "XPosterFunction" \| summarize avg(duration)` | > 60 000 ms | Sev 3 – Informational | +| Function downtime | Built-in **Availability** test on the Function App URL | < 100% | Sev 1 – Error | + +#### IaC: Bicep Snippet for Alert Provisioning + +Use the following Bicep snippet to provision the consecutive-errors alert rule as Infrastructure-as-Code: + +```bicep +resource consecutiveErrorsAlert 'Microsoft.Insights/scheduledQueryRules@2022-06-15' = { + name: 'XPoster-ConsecutiveErrors' + location: resourceGroup().location + properties: { + description: 'Fires when more than 3 errors are logged within 1 hour' + severity: 2 + enabled: true + scopes: [ + appInsights.id + ] + evaluationFrequency: 'PT5M' + windowSize: 'PT1H' + criteria: { + allOf: [ + { + query: 'traces | where severityLevel >= 3 | summarize errorCount = count()' + timeAggregation: 'Count' + operator: 'GreaterThan' + threshold: 3 + failingPeriods: { + numberOfEvaluationPeriods: 1 + minFailingPeriodsToAlert: 1 + } + } + ] + } + actions: { + actionGroups: [ + actionGroup.id + ] + } + } +} +``` + +> πŸ“– Full KQL queries, alert thresholds, and live debugging instructions: [docs/monitoring.md](docs/monitoring.md). + +--- + +## Roadmap + +### βœ… Phase 1: Foundation (Complete) +- [x] Azure Function setup +- [x] Multi-platform sender architecture +- [x] AI integration (gpt-4.1-nano, gpt-image-1.5) +- [x] Twitter/X publishing +- [x] LinkedIn publishing +- [x] RSS feed parsing +- [x] CI/CD pipeline + +### 🚧 Phase 2: Stabilization (In Progress) +- [ ] AI migration to Azure Foundry +- [ ] Linkedin auto-update authorization token +- [ ] Configuration externalization +- [ ] Enhanced error handling +- [ ] Comprehensive testing (80%+ coverage) + +### πŸ“… Phase 3: Intelligence (Q1 2026) +- [ ] Post-publication analytics +- [ ] ML-based optimal timing +- [ ] Sentiment analysis +- [ ] A/B testing framework +- [ ] Trending hashtag detection +- [ ] Multi-language support + +### 🎨 Phase 4: Admin Dashboard (Q2 2026) +- [ ] Web based UI +- [ ] Real-time analytics +- [ ] Manual post scheduling +- [ ] Content calendar +- [ ] Performance metrics +- [ ] Mobile app (MAUI) + +### 🌍 Phase 5: Expansion (Q3 2026) +- [ ] Instagram publishing (complete setup) +- [ ] Threads (Meta) integration +- [ ] Mastodon support +- [ ] BlueSky protocol +- [ ] YouTube Shorts +- [ ] Podcast automation + +--- + +## Contributing + +Contributions, issues, and feature requests are welcome! + +### How to Contribute + +1. **Fork** the project +2. **Create** your feature branch (`git checkout -b feature/AmazingFeature`) +3. **Commit** your changes (`git commit -m 'Add some AmazingFeature'`) +4. **Push** to the branch (`git push origin feature/AmazingFeature`) +5. **Open** a Pull Request + +### Guidelines + +- Follow C# (.NET) coding conventions +- Add unit tests for new features +- Update documentation +- Keep commits atomic and descriptive +- Respect existing design patterns + +### Coding Standards + +```csharp +// βœ… Good +public async Task GenerateAsync() +{ + var summary = await _aiService.GetSummaryAsync(content, maxLength); + if (string.IsNullOrWhiteSpace(summary)) + { + _logger.LogWarning("Empty summary generated"); + return null; + } + return new Post { Content = summary }; +} + +// ❌ Avoid +public async Task GenerateAsync() { + var summary = await _aiService.GetSummaryAsync(content, maxLength); + if (summary == null || summary == "") return null; + return new Post { Content = summary }; +} +``` + +--- + +## License + +This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. + +``` +MIT License + +Copyright (c) 2025 Marco Cavallo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +``` + + +--- + +## Author + +**Marco Cavallo** + +- 🌐 Website: [xposter.artcava.net](https://xposter.artcava.net) +- πŸ’Ό LinkedIn: [Marco Cavallo](https://linkedin.com/in/artcava) +- 🐦 Twitter: [@artcava](https://twitter.com/artcava) +- πŸ“§ Email: cavallo.marco@gmail.com +- 🏒 Location: Turin, Italy + +--- + +## Acknowledgments + +- [Azure Functions](https://azure.microsoft.com/services/functions/) - Serverless platform +- [OpenAI](https://openai.com/) - AI models (gpt-4.1-nano, gpt-image-1.5) +- [LinqToTwitter](https://github.com/JoeMayo/LinqToTwitter) - Twitter API wrapper +- [.NET Foundation](https://dotnetfoundation.org/) - Framework and community + +--- + +## Support + +- **Issues**: [GitHub Issues](https://github.com/artcava/XPoster/issues) +- **Discussions**: [GitHub Discussions](https://github.com/artcava/XPoster/discussions) +- **Email**: cavallo.marco@gmail.com + +--- + +## Star History + +If you find this project useful, consider leaving a ⭐ on GitHub! + +[![Star History Chart](https://api.star-history.com/svg?repos=artcava/XPoster&type=Date)](https://star-history.com/#artcava/XPoster&Date) + +--- + +
+ +**Made with ❀️ in Turin, Italy** + +[🏠 Homepage](https://xposter.artcava.net/) β€’ +[πŸ“– Documentation](docs/index.md) β€’ +[πŸ› Report Bug](https://github.com/artcava/XPoster/issues) β€’ +[πŸ’‘ Request Feature](https://github.com/artcava/XPoster/issues) + +
diff --git a/XPoster.sln b/XPoster.sln index 9053d92..e45d464 100644 --- a/XPoster.sln +++ b/XPoster.sln @@ -7,6 +7,25 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XPoster", "src\XPoster.cspr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XPoster.Tests", "tests\XPoster.Tests.csproj", "{4CD93D7E-E307-4F1E-BE01-831C216E6F80}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + ARCHITECTURE.md = ARCHITECTURE.md + CHANGELOG.md = CHANGELOG.md + CONTRIBUTING.md = CONTRIBUTING.md + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{51F62806-2C94-4BC7-A69E-266C207AB893}" + ProjectSection(SolutionItems) = preProject + docs\analysis-linkedin-token-auto-refresh.md = docs\analysis-linkedin-token-auto-refresh.md + docs\configuration.md = docs\configuration.md + docs\deployment.md = docs\deployment.md + docs\extending-xposter.md = docs\extending-xposter.md + docs\getting-started.md = docs\getting-started.md + docs\index.md = docs\index.md + docs\monitoring.md = docs\monitoring.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +44,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {51F62806-2C94-4BC7-A69E-266C207AB893} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9CCF3EE4-D229-4EAE-9A97-C48A6E280027} EndGlobalSection