Social auto-posting (phase 1): daily applicant digest to Telegram/Bale + Instagram caption
Adds a «شبکههای اجتماعی» admin section + scheduler that publishes a daily «کادر آمادهبهکار امروز» digest: - AppSetting: social toggles, posts-per-day, editable header/footer, per-channel bot token + chat id (Telegram, Bale), Instagram enable + extra hashtags, proxy toggle, last-posted timestamp (+ migration). - SocialPostService: builds today's talent digest as text, posts to Telegram and Bale via their bot sendMessage APIs (proxy-aware), and produces an Instagram caption + auto hashtags (role/city based). - SocialPostWorker: posts N times/day, evenly spaced, self-paced; reads settings live so it's togglable without redeploy. - /Admin/Social: credentials + header/footer + posts/day, live preview of today's message, «ارسال اکنون» button, and an Instagram caption pack with copy button (semi-automatic — you post the image manually). - Nav link added. Telegram/Bale post as TEXT (per request). The Vazirmatn image card for Instagram is phase 2 (needs SkiaSharp+HarfBuzz + a TTF font). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.SocialModel
|
||||
@{
|
||||
ViewData["Title"] = "شبکههای اجتماعی";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>شبکههای اجتماعی</h1>
|
||||
<p class="muted">انتشار خودکار «کادر آمادهبهکار امروز» در تلگرام و بله (متن) و بستهی کپشن/هشتگ برای اینستاگرام.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
@if (Model.Message is not null) { <div class="alert alert-success">✓ @Model.Message</div> }
|
||||
@if (Model.Error is not null) { <div class="alert alert-error">⚠ @Model.Error</div> }
|
||||
|
||||
<div class="layout-2">
|
||||
<div>
|
||||
<form method="post" class="card card-pad">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" name="SocialEnabled" value="true" checked="@Model.SocialEnabled" />
|
||||
<span class="t-body"><span>انتشار خودکار روشن باشد</span><span class="t-hint">روزانه چند بار، بهصورت زمانبندیشده ارسال میشود.</span></span>
|
||||
</label>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:0 0 160px;"><label>تعداد پست در روز</label><input type="number" name="SocialPostsPerDay" min="1" max="24" value="@Model.SocialPostsPerDay" dir="ltr" /></div>
|
||||
<label class="proxy-toggle" style="align-self:end;"><input type="checkbox" name="SocialUseProxy" value="true" checked="@Model.SocialUseProxy" /> ارسال از طریق پروکسی (برای تلگرام)</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>سرتیتر پیام (Header)</label>
|
||||
<textarea name="SocialHeader" rows="2" placeholder="مثلاً: 🩺 همکادر | مرجع شیفت و استخدام کادر درمان">@Model.SocialHeader</textarea>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>پاورقی پیام (Footer)</label>
|
||||
<textarea name="SocialFooter" rows="2" placeholder="مثلاً: ثبت رایگان آگهی در hamkadr.ir | @@hamkadr">@Model.SocialFooter</textarea>
|
||||
</div>
|
||||
|
||||
<div class="source-box">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" name="SocialTelegramEnabled" value="true" checked="@Model.SocialTelegramEnabled" />
|
||||
<span class="t-body"><span>📨 تلگرام (متن)</span><span class="t-hint">با بات تلگرام در کانال شما پست میشود.</span></span>
|
||||
</label>
|
||||
<div class="filter-group"><label>توکن بات تلگرام</label><input type="password" name="SocialTelegramBotToken" value="@Model.SocialTelegramBotToken" dir="ltr" placeholder="123456:ABC-..." /></div>
|
||||
<div class="filter-group"><label>شناسه کانال/چت</label><input type="text" name="SocialTelegramChatId" value="@Model.SocialTelegramChatId" dir="ltr" placeholder="@@your_channel یا -100..." />
|
||||
<p class="muted" style="font-size:11px; margin:4px 0 0;">بات باید ادمینِ کانال باشد.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="source-box">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" name="SocialBaleEnabled" value="true" checked="@Model.SocialBaleEnabled" />
|
||||
<span class="t-body"><span>💬 بله (متن)</span></span>
|
||||
</label>
|
||||
<div class="filter-group"><label>توکن بات بله</label><input type="password" name="SocialBaleBotToken" value="@Model.SocialBaleBotToken" dir="ltr" /></div>
|
||||
<div class="filter-group"><label>شناسه کانال/چت بله</label><input type="text" name="SocialBaleChatId" value="@Model.SocialBaleChatId" dir="ltr" placeholder="@@your_channel یا عدد" /></div>
|
||||
</div>
|
||||
|
||||
<div class="source-box">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" name="SocialInstagramEnabled" value="true" checked="@Model.SocialInstagramEnabled" />
|
||||
<span class="t-body"><span>📷 اینستاگرام (نیمهخودکار)</span><span class="t-hint">کپشن و هشتگ آماده میشود؛ تصویر و انتشار را دستی انجام میدهی.</span></span>
|
||||
</label>
|
||||
<div class="filter-group"><label>هشتگهای اضافه (با فاصله یا خط جدید)</label>
|
||||
<textarea name="InstagramHashtags" rows="2" dir="ltr" placeholder="#استخدام_پرستار #شیفت_تهران">@Model.InstagramHashtags</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-save">
|
||||
<button type="submit" asp-page-handler="Save" class="btn btn-accent btn-block btn-lg">ذخیره تنظیمات</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" style="margin-top:12px;">
|
||||
<button type="submit" asp-page-handler="SendNow" class="btn btn-outline btn-block">📤 ارسال اکنون (تلگرام/بله)</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">پیشنمایش پیام امروز</h3>
|
||||
@if (Model.Preview is null || Model.Preview.Count == 0)
|
||||
{
|
||||
<p class="muted">امروز هنوز موردِ «آماده به کار» تازهای ثبت نشده است.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted" style="font-size:12px;">@JalaliDate.ToPersianDigits(Model.Preview.Count.ToString()) مورد — همین متن به تلگرام/بله میرود.</p>
|
||||
<pre style="white-space:pre-wrap; font-family:inherit; background:var(--bg); border:1px solid var(--line); border-radius:10px; padding:12px; font-size:13px; margin:0;">@Model.Preview.TelegramText</pre>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.SocialInstagramEnabled && Model.Preview is not null && Model.Preview.Count > 0)
|
||||
{
|
||||
<div class="card card-pad" style="margin-top:12px;">
|
||||
<h3 style="margin-top:0;">📷 بستهی اینستاگرام</h3>
|
||||
<label style="font-size:12px; font-weight:700;">کپشن (با هشتگ):</label>
|
||||
<textarea id="igCaption" rows="8" style="width:100%; font-size:12.5px;">@Model.Preview.InstagramCaption</textarea>
|
||||
<button type="button" class="btn btn-outline btn-block" style="margin-top:6px;" onclick="navigator.clipboard.writeText(document.getElementById('igCaption').value); this.textContent='کپی شد ✓';">کپی کپشن</button>
|
||||
<p class="muted" style="font-size:11px; margin:8px 0 0;">تصویر کارت با فونت وزیر در نسخهی بعدی اضافه میشود؛ فعلاً کپشن/هشتگ را کپی کن و در اینستاگرام پست کن.</p>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using JobsMedical.Web.Services.Scraping;
|
||||
using JobsMedical.Web.Services.Social;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class SocialModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly SocialPostService _social;
|
||||
|
||||
public SocialModel(AppDbContext db, SettingsService settings, SocialPostService social)
|
||||
{
|
||||
_db = db; _settings = settings; _social = social;
|
||||
}
|
||||
|
||||
[TempData] public string? Message { get; set; }
|
||||
[TempData] public string? Error { get; set; }
|
||||
|
||||
public SocialDigest? Preview { get; private set; }
|
||||
|
||||
[BindProperty] public bool SocialEnabled { get; set; }
|
||||
[BindProperty] public int SocialPostsPerDay { get; set; }
|
||||
[BindProperty] public string? SocialHeader { get; set; }
|
||||
[BindProperty] public string? SocialFooter { get; set; }
|
||||
[BindProperty] public bool SocialUseProxy { get; set; }
|
||||
[BindProperty] public bool SocialTelegramEnabled { get; set; }
|
||||
[BindProperty] public string? SocialTelegramBotToken { get; set; }
|
||||
[BindProperty] public string? SocialTelegramChatId { get; set; }
|
||||
[BindProperty] public bool SocialBaleEnabled { get; set; }
|
||||
[BindProperty] public string? SocialBaleBotToken { get; set; }
|
||||
[BindProperty] public string? SocialBaleChatId { get; set; }
|
||||
[BindProperty] public bool SocialInstagramEnabled { get; set; }
|
||||
[BindProperty] public string? InstagramHashtags { get; set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var s = await _settings.GetAsync();
|
||||
SocialEnabled = s.SocialEnabled;
|
||||
SocialPostsPerDay = s.SocialPostsPerDay;
|
||||
SocialHeader = s.SocialHeader;
|
||||
SocialFooter = s.SocialFooter;
|
||||
SocialUseProxy = s.SocialUseProxy;
|
||||
SocialTelegramEnabled = s.SocialTelegramEnabled;
|
||||
SocialTelegramBotToken = s.SocialTelegramBotToken;
|
||||
SocialTelegramChatId = s.SocialTelegramChatId;
|
||||
SocialBaleEnabled = s.SocialBaleEnabled;
|
||||
SocialBaleBotToken = s.SocialBaleBotToken;
|
||||
SocialBaleChatId = s.SocialBaleChatId;
|
||||
SocialInstagramEnabled = s.SocialInstagramEnabled;
|
||||
InstagramHashtags = s.InstagramHashtags;
|
||||
|
||||
Preview = await _social.BuildDigestAsync(s);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSaveAsync()
|
||||
{
|
||||
var s = await _settings.GetAsync();
|
||||
s.SocialEnabled = SocialEnabled;
|
||||
s.SocialPostsPerDay = Math.Clamp(SocialPostsPerDay, 1, 24);
|
||||
s.SocialHeader = SocialHeader?.Trim();
|
||||
s.SocialFooter = SocialFooter?.Trim();
|
||||
s.SocialUseProxy = SocialUseProxy;
|
||||
s.SocialTelegramEnabled = SocialTelegramEnabled;
|
||||
s.SocialTelegramBotToken = SocialTelegramBotToken?.Trim();
|
||||
s.SocialTelegramChatId = SocialTelegramChatId?.Trim();
|
||||
s.SocialBaleEnabled = SocialBaleEnabled;
|
||||
s.SocialBaleBotToken = SocialBaleBotToken?.Trim();
|
||||
s.SocialBaleChatId = SocialBaleChatId?.Trim();
|
||||
s.SocialInstagramEnabled = SocialInstagramEnabled;
|
||||
s.InstagramHashtags = InstagramHashtags?.Trim();
|
||||
s.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
Message = "تنظیمات شبکههای اجتماعی ذخیره شد.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSendNowAsync()
|
||||
{
|
||||
var r = await _social.PostAsync();
|
||||
if (r.Count == 0) Error = r.Error ?? "موردی برای انتشار نبود.";
|
||||
else
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (r.TelegramOk) parts.Add("تلگرام ✓");
|
||||
if (r.BaleOk) parts.Add("بله ✓");
|
||||
Message = parts.Count > 0
|
||||
? $"ارسال شد ({string.Join("، ", parts)}) — {JalaliDate.ToPersianDigits(r.Count.ToString())} مورد."
|
||||
: "هیچ کانالی ارسال نشد؛ توکن/شناسه و فعالبودن را بررسی کن.";
|
||||
if (r.Error is not null && parts.Count == 0) Error = r.Error;
|
||||
}
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<a class="@(On("/Admin/Users") ? "active" : null)" asp-page="/Admin/Users">👥 کاربران</a>
|
||||
<a class="@(On("/Admin/Reports") ? "active" : null)" asp-page="/Admin/Reports">🛡️ گزارشها</a>
|
||||
<a class="@(On("/Admin/Broadcast") ? "active" : null)" asp-page="/Admin/Broadcast">📣 اعلان همگانی</a>
|
||||
<a class="@(On("/Admin/Social") ? "active" : null)" asp-page="/Admin/Social">📡 شبکههای اجتماعی</a>
|
||||
<a class="@(On("/Admin/Settings") ? "active" : null)" asp-page="/Admin/Settings">⚙️ تنظیمات</a>
|
||||
}
|
||||
else if (User.IsInRole("FacilityAdmin"))
|
||||
|
||||
Reference in New Issue
Block a user