Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Menza info #418

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Shouldly" Version="4.1.0" />
<PackageVersion Include="Html2Markdown" Version="5.1.0.703" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.46" />
</ItemGroup>
</Project>
53 changes: 53 additions & 0 deletions src/HonzaBotner.Discord.Services/Jobs/SuzJobProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using HonzaBotner.Discord.Services.Helpers;
using HonzaBotner.Discord.Services.Options;
using HonzaBotner.Scheduler.Contract;
using HonzaBotner.Services.Contract;
using HonzaBotner.Services.Contract.Dto;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace HonzaBotner.Discord.Services.Jobs;

[Cron("0 30 10 * * 1-5")]
public class SuzJobProvider : IJob
{
private readonly ILogger<SuzJobProvider> _logger;

private readonly DiscordWrapper _discord;
private readonly ICanteenService _canteenService;
private readonly IGuildProvider _guildProvider;
private readonly CommonCommandOptions _commonOptions;

public SuzJobProvider(
ILogger<SuzJobProvider> logger,
DiscordWrapper discord,
ICanteenService canteenService,
IGuildProvider guildProvider,
IOptions<CommonCommandOptions> commonOptions)
{
_logger = logger;
_discord = discord;
_canteenService = canteenService;
_guildProvider = guildProvider;
_commonOptions = commonOptions.Value;
}

public string Name => "suz-agata";

public async Task ExecuteAsync(CancellationToken cancellationToken)
{
IList<CanteenDto> canteens = await _canteenService.ListCanteensAsync(true, cancellationToken);

DiscordGuild guild = await _guildProvider.GetCurrentGuildAsync();
guild.ListActiveThreadsAsync()
_discord.Client.SendMessageAsync()
}
}
9 changes: 9 additions & 0 deletions src/HonzaBotner.Services.Contract/Dto/CanteenDishDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace HonzaBotner.Services.Contract.Dto;

public record CanteenDishDto(
string DishType,
string Name,
string Amount,
string StudentPrice,
string OtherPrice,
string PhotoLink = "");
5 changes: 5 additions & 0 deletions src/HonzaBotner.Services.Contract/Dto/CanteenDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Collections.Generic;

namespace HonzaBotner.Services.Contract.Dto;

public record CanteenDto(int Id, string Name, bool Open, IReadOnlyList<CanteenDishDto>? TodayDishes = null);
13 changes: 13 additions & 0 deletions src/HonzaBotner.Services.Contract/ICanteenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using HonzaBotner.Services.Contract.Dto;

namespace HonzaBotner.Services.Contract;

public interface ICanteenService
{
Task<IList<CanteenDto>> ListCanteensAsync(bool onlyOpen = false, CancellationToken cancellationToken = default);
Task<CanteenDto> GetCurrentMenuAsync(CanteenDto canteen, CancellationToken cancellationToken = default);
}
54 changes: 54 additions & 0 deletions src/HonzaBotner.Services.Test/SuzCanteenServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using HonzaBotner.Services.Contract;
using HonzaBotner.Services.Contract.Dto;
using Xunit;

namespace HonzaBotner.Services.Test;

public class SuzCanteenServiceTest
{
private static readonly CanteenDto Technicka = new(3, "Technická menza", true);

[Fact]
public async void ListCanteens()
{
ICanteenService canteenService = new SuzCanteenService(new HttpClient()); // TODO: Init

var canteens = (await canteenService.ListCanteensAsync()).ToList();

Assert.NotEmpty(canteens);
Assert.All(canteens, dto =>
{
Assert.NotEmpty(dto.Name);
Assert.NotEqual(0, dto.Id);
});

Assert.Contains(Technicka, canteens);
}

[Fact]
public async void GetCurrentMenu()
{
ICanteenService canteenService = new SuzCanteenService(new HttpClient()); // TODO: Init
var canteens = await canteenService.ListCanteensAsync(true);

foreach (CanteenDto canteen in canteens)
{
var canteenWithMenu = await canteenService.GetCurrentMenuAsync(canteen);
// Method modifies only today dishes
Assert.Equal(canteen, canteenWithMenu with { TodayDishes = canteen.TodayDishes });
Assert.NotNull(canteenWithMenu);

foreach (var dish in canteenWithMenu.TodayDishes!)
{
Assert.NotEmpty(dish.DishType);
Assert.NotEqual("Jiné", dish.DishType);
Assert.NotEmpty(dish.Name);
Assert.NotEmpty(dish.StudentPrice);
Assert.NotEmpty(dish.OtherPrice);
}
}
}
}
1 change: 1 addition & 0 deletions src/HonzaBotner.Services/HonzaBotner.Services.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Html2Markdown" />
<PackageReference Include="HtmlAgilityPack" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HonzaBotner.Database\HonzaBotner.Database.csproj" />
Expand Down
128 changes: 128 additions & 0 deletions src/HonzaBotner.Services/SuzCanteenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using HonzaBotner.Services.Contract;
using HonzaBotner.Services.Contract.Dto;
using HtmlAgilityPack;
using Microsoft.Extensions.Caching.Memory;

namespace HonzaBotner.Services;

public class SuzCanteenService: ICanteenService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _memoryCache;

public SuzCanteenService(HttpClient httpClient, IMemoryCache memoryCache)
{
_httpClient = httpClient;
_memoryCache = memoryCache;
}

public async Task<IList<CanteenDto>> ListCanteensAsync(bool onlyOpen = false, CancellationToken cancellationToken = default)
{
const string cacheKey = $"{nameof(SuzCanteenService)}_{nameof(ListCanteensAsync)}";

if (!_memoryCache.TryGetValue(cacheKey, out List<CanteenDto> canteens))
{

const string url = "https://agata.suz.cvut.cz/jidelnicky/index.php";
string pageContent = await _httpClient.GetStringAsync(url, cancellationToken);

var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(pageContent);

canteens = htmlDoc.DocumentNode.SelectNodes("//ul[@id='menzy']/li/a")
.Select(node =>
{
int.TryParse(node.Id.Replace("podSh", ""), out int id);
bool open = node.SelectSingleNode($"{node.XPath}/img")
.GetAttributeValue("src", "closed").Contains("Otevreno");
string name = node.InnerText.Trim();
return new CanteenDto(id, name, open);
}).ToList();

var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(1));

_memoryCache.Set(cacheKey, canteens, cacheEntryOptions);
}

if (onlyOpen)
return canteens.Where(c => c.Open).ToList();
return canteens.ToList();
}

public async Task<CanteenDto> GetCurrentMenuAsync(CanteenDto canteen, CancellationToken cancellationToken = default)
{
const string url = "https://agata.suz.cvut.cz/jidelnicky/index.php?clPodsystem={0}";
string pageContent = await _httpClient.GetStringAsync(string.Format(url, canteen.Id), cancellationToken);

var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(pageContent);

var rows = htmlDoc.DocumentNode
.SelectNodes("//div[@class='data']/table[@class='table table-condensed']/tbody/tr");
if (rows is null)
{
// TODO: Log and maybe say something to user?
return canteen with { TodayDishes = ArraySegment<CanteenDishDto>.Empty};
}

var dishes = new List<CanteenDishDto>();

string currentDishType = "Jiné";

foreach (var row in rows)
{
if (row is null) continue;

var th = row.ChildNodes["th"];
if (th is not null)
{
currentDishType = th.InnerText.Trim();
continue;
}

var tds = row.SelectNodes("td");
if (tds is null) continue;

string amount = TrimHtml(tds[1].InnerText);
string name = TrimHtml(tds[2].InnerText);
string photo = ParsePhoto(tds[4]);

string studentPrice = TrimHtml(tds[5].InnerText);
string otherPrice = TrimHtml(tds[6].InnerText);

dishes.Add(new CanteenDishDto(currentDishType, name, amount, studentPrice, otherPrice, photo));
}


return canteen with { TodayDishes = dishes };
}


private static string ParsePhoto(HtmlNode node)
{
var photo = node.Descendants("a")?.FirstOrDefault();

if (photo is null)
{
return string.Empty;
}

string link = photo.GetAttributeValue("href", string.Empty);

if (string.IsNullOrEmpty(link)) return link;

return $"https://agata.suz.cvut.cz/jidelnicky/{link}";
}

private static string TrimHtml(string text)
{
return text.Replace("&nbsp;", " ").Trim();
}
}
3 changes: 3 additions & 0 deletions src/HonzaBotner/appsettings.CvutFit.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"issueTrackerUrl": "https://github.com/fit-ctu-discord/honza-botner/issues",
"changelogUrl": "https://github.com/fit-ctu-discord/honza-botner/releases"
},
"SuzCanteenOptions": {
"PublishThre"
},
"DiscordRoles": {
"AuthenticatedRoleIds": [681559148546359432, 686867633085480970],
"AuthRoleMapping": {
Expand Down