Init commit
This commit is contained in:
commit
f82817d0f8
|
@ -0,0 +1,5 @@
|
||||||
|
data/config.json
|
||||||
|
|
||||||
|
.ionide
|
||||||
|
src/Bot/obj
|
||||||
|
src/Bot/bin
|
|
@ -0,0 +1,20 @@
|
||||||
|
# WikiMathBot
|
||||||
|
|
||||||
|
This is a bot originally made for keeping track of whenever the newest exercise came out for the subject `MA0301`
|
||||||
|
|
||||||
|
In order to make the bot functional, it needs a configuration file at `src/Bot/config.json` that looks like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"BotToken": "<secret discord bot token>",
|
||||||
|
"Class": "ma0301",
|
||||||
|
"Year": "2021v",
|
||||||
|
"SecondsBetweenUpdate": 3600.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The class and year variables are part of the url to the page:
|
||||||
|
|
||||||
|
`https://wiki.math.ntnu.no/<class>/<year>/start`
|
||||||
|
|
||||||
|
The project can be run by executing `dotnet run` from within the `src/Bot` directory
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.26124.0
|
||||||
|
MinimumVisualStudioVersion = 15.0.26124.0
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7D95551A-EDA1-409F-B788-06331223141D}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Bot", "src\Bot\Bot.fsproj", "{43083459-0352-497A-9514-2E17FCE3E783}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{43083459-0352-497A-9514-2E17FCE3E783} = {7D95551A-EDA1-409F-B788-06331223141D}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -0,0 +1,32 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Config.fs" />
|
||||||
|
<Compile Include="Channels.fs" />
|
||||||
|
<Compile Include="WikiMathParser.fs" />
|
||||||
|
<Compile Include="BotCommands.fs" />
|
||||||
|
<Compile Include="WebsiteCheckLoop.fs" />
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="../../data/channels.dat" />
|
||||||
|
<Content Include="../../data/config.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net" Version="1.0.2" />
|
||||||
|
<PackageReference Include="DSharpPlus" Version="3.2.3" />
|
||||||
|
<PackageReference Include="DSharpPlus.CommandsNext" Version="3.2.3" />
|
||||||
|
<PackageReference Include="FSharp.Data" Version="4.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module BotCommands =
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
open DSharpPlus.CommandsNext
|
||||||
|
open DSharpPlus.CommandsNext.Attributes
|
||||||
|
|
||||||
|
open Channels
|
||||||
|
|
||||||
|
type BotCommands() =
|
||||||
|
[<Command("hi")>]
|
||||||
|
member public self.hi(ctx:CommandContext) =
|
||||||
|
async { ctx.RespondAsync "Hi there" |> ignore } |> Async.StartAsTask :> Task
|
||||||
|
|
||||||
|
[<Command("echo")>]
|
||||||
|
member public self.echo(ctx:CommandContext) (message:string) =
|
||||||
|
async { ctx.RespondAsync message |> ignore } |> Async.StartAsTask :> Task
|
||||||
|
|
||||||
|
[<Command("toggle")>]
|
||||||
|
member public self.toggle(ctx:CommandContext) =
|
||||||
|
async {
|
||||||
|
toggleChannel ctx.Channel.Id
|
||||||
|
|> fun channelGotAdded ->
|
||||||
|
match channelGotAdded with
|
||||||
|
| true -> ctx.RespondAsync "This is now my channel :)" |> ignore
|
||||||
|
| false -> ctx.RespondAsync "This is now your channel (:" |> ignore
|
||||||
|
} |> Async.StartAsTask :> Task
|
|
@ -0,0 +1,41 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module Channels =
|
||||||
|
|
||||||
|
open FSharp.Data
|
||||||
|
open System.IO
|
||||||
|
|
||||||
|
let private filepath = __SOURCE_DIRECTORY__ + "/../../data/channels.dat"
|
||||||
|
|
||||||
|
let mutable channels =
|
||||||
|
File.ReadLines(filepath)
|
||||||
|
|> Seq.map (fun line -> line.ToString().AsInteger64())
|
||||||
|
|> Seq.map (uint64)
|
||||||
|
|> set
|
||||||
|
|
||||||
|
let private updateChannels newChannels =
|
||||||
|
newChannels
|
||||||
|
|> Seq.map (fun i -> i.ToString())
|
||||||
|
|> Seq.toList
|
||||||
|
|> fun lines -> File.WriteAllLines(filepath, lines)
|
||||||
|
channels <- newChannels
|
||||||
|
|
||||||
|
let private removeChannel (channelId:uint64) =
|
||||||
|
channels.Remove(channelId)
|
||||||
|
|> updateChannels
|
||||||
|
|
||||||
|
let private addChannel (channelId:uint64) =
|
||||||
|
channels.Add(channelId)
|
||||||
|
|> updateChannels
|
||||||
|
|
||||||
|
let toggleChannel (channelId:uint64) =
|
||||||
|
match channelId with
|
||||||
|
| channelId when channels.Contains(channelId) ->
|
||||||
|
removeChannel channelId
|
||||||
|
false
|
||||||
|
|
||||||
|
| channelId ->
|
||||||
|
addChannel channelId
|
||||||
|
true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module Config =
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open Microsoft.Extensions.Configuration
|
||||||
|
|
||||||
|
let private getConfig =
|
||||||
|
let builder = new ConfigurationBuilder()
|
||||||
|
do builder.SetBasePath( Directory.GetCurrentDirectory() + "/../../data" ) |> ignore
|
||||||
|
do builder.AddJsonFile("config.json") |> ignore
|
||||||
|
builder.Build()
|
||||||
|
|
||||||
|
let config = getConfig
|
|
@ -0,0 +1,46 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module core =
|
||||||
|
|
||||||
|
open System
|
||||||
|
open DSharpPlus
|
||||||
|
open DSharpPlus.CommandsNext
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
open Config
|
||||||
|
open WebsiteCheckLoop
|
||||||
|
open BotCommands
|
||||||
|
open WikiMathParser
|
||||||
|
|
||||||
|
let getDiscordConfig =
|
||||||
|
let conf = new DiscordConfiguration()
|
||||||
|
conf.set_Token config.["BotToken"]
|
||||||
|
conf.set_TokenType TokenType.Bot
|
||||||
|
conf.set_UseInternalLogHandler true
|
||||||
|
conf.set_LogLevel LogLevel.Debug
|
||||||
|
conf
|
||||||
|
|
||||||
|
let getCommandsConfig =
|
||||||
|
let conf = new CommandsNextConfiguration()
|
||||||
|
conf.set_StringPrefix "!"
|
||||||
|
conf
|
||||||
|
|
||||||
|
let client = new DiscordClient(getDiscordConfig)
|
||||||
|
let commands = client.UseCommandsNext(getCommandsConfig)
|
||||||
|
|
||||||
|
let mainTask =
|
||||||
|
let mutable previousResults = getStatus
|
||||||
|
|
||||||
|
async {
|
||||||
|
client.add_MessageCreated(fun e -> async { Console.WriteLine e.Message.Content } |> Async.StartAsTask :> _)
|
||||||
|
commands.RegisterCommands<BotCommands>()
|
||||||
|
client.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously
|
||||||
|
do! scrapePeriodically (scrapeFun client previousResults
|
||||||
|
>> fun results -> previousResults <- results)
|
||||||
|
do! Async.AwaitTask(Task.Delay(-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
Async.RunSynchronously(mainTask)
|
||||||
|
0
|
|
@ -0,0 +1,47 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module WebsiteCheckLoop =
|
||||||
|
|
||||||
|
open System
|
||||||
|
open DSharpPlus
|
||||||
|
open Channels
|
||||||
|
|
||||||
|
let private log message =
|
||||||
|
printfn "[%A] [Info] %s" DateTime.Now message
|
||||||
|
|
||||||
|
let private sendToChannelWith (client:DiscordClient) message id =
|
||||||
|
log <| sprintf "Sending message to channel: %A" id
|
||||||
|
id
|
||||||
|
|> client.GetChannelAsync
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|> Async.RunSynchronously
|
||||||
|
|> fun channel ->
|
||||||
|
client.SendMessageAsync(channel, message)
|
||||||
|
|> Async.AwaitTask
|
||||||
|
|> Async.RunSynchronously
|
||||||
|
|> ignore
|
||||||
|
|
||||||
|
let private formatMessage message link =
|
||||||
|
message + "\n" + link
|
||||||
|
|
||||||
|
let scrapeFun client (previousListOfResults:List<string * string>) (listOfResults:List<string * string>) =
|
||||||
|
log "Scraping website"
|
||||||
|
|
||||||
|
match previousListOfResults, listOfResults with
|
||||||
|
| (previousListOfResults, listOfResults) when previousListOfResults = listOfResults ->
|
||||||
|
|
||||||
|
channels
|
||||||
|
|> Seq.iter (fun id -> sendToChannelWith client (formatMessage <|| Seq.head listOfResults) id)
|
||||||
|
|
||||||
|
Seq.head listOfResults
|
||||||
|
||> formatMessage
|
||||||
|
|> fun s -> s.Split("\n")
|
||||||
|
|> Seq.map (fun s -> "\t" + s)
|
||||||
|
|> Seq.fold (fun a b -> a + "\n" + b) ""
|
||||||
|
|> sprintf "Found following update: \n%s"
|
||||||
|
|> log
|
||||||
|
|
||||||
|
| (_, _) ->
|
||||||
|
log "No new content found"
|
||||||
|
|
||||||
|
listOfResults
|
|
@ -0,0 +1,35 @@
|
||||||
|
namespace Bot
|
||||||
|
|
||||||
|
module WikiMathParser =
|
||||||
|
|
||||||
|
open System
|
||||||
|
open FSharp.Data
|
||||||
|
open Config
|
||||||
|
|
||||||
|
let private page = HtmlDocument.Load(sprintf "https://wiki.math.ntnu.no/%s/%s/start" config.["Class"] config.["Year"])
|
||||||
|
|
||||||
|
let private findPDFLink (node:HtmlNode) =
|
||||||
|
node.CssSelect(".mf_pdf")
|
||||||
|
|> fun l -> match l with
|
||||||
|
| l when Seq.length l = 0 -> ""
|
||||||
|
| l -> HtmlNode.attributeValue "href" (Seq.head l)
|
||||||
|
|> (+) "https://wiki.math.ntnu.no"
|
||||||
|
|
||||||
|
let getStatus =
|
||||||
|
(page.CssSelect ".level2 > ul > .level1")
|
||||||
|
|> Seq.map (fun (x:HtmlNode) -> (x.InnerText().TrimStart(), findPDFLink x ))
|
||||||
|
|> Seq.filter (fun (x:string, _) -> x.Contains("Problem Set"))
|
||||||
|
|> Seq.toList
|
||||||
|
|
||||||
|
|
||||||
|
let private timer = new Timers.Timer( config.["SecondsBetweenUpdate"].AsFloat() * 1000.0)
|
||||||
|
let private waitAPeriod = Async.AwaitEvent (timer.Elapsed) |> Async.Ignore
|
||||||
|
|
||||||
|
let scrapePeriodically (callback: List<String * String> -> unit ) =
|
||||||
|
timer.Start()
|
||||||
|
async {
|
||||||
|
while true do
|
||||||
|
Async.RunSynchronously waitAPeriod
|
||||||
|
getStatus
|
||||||
|
|> callback
|
||||||
|
}
|
Reference in New Issue