Init commit
This commit is contained in:
commit
f82817d0f8
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
data/config.json
|
||||
|
||||
.ionide
|
||||
src/Bot/obj
|
||||
src/Bot/bin
|
20
README.md
Normal file
20
README.md
Normal file
@ -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
|
39
WikiMathBot.sln
Normal file
39
WikiMathBot.sln
Normal file
@ -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
data/channels.dat
Normal file
0
data/channels.dat
Normal file
32
src/Bot/Bot.fsproj
Normal file
32
src/Bot/Bot.fsproj
Normal file
@ -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>
|
28
src/Bot/BotCommands.fs
Normal file
28
src/Bot/BotCommands.fs
Normal file
@ -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
|
41
src/Bot/Channels.fs
Normal file
41
src/Bot/Channels.fs
Normal file
@ -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
|
||||
|
||||
|
14
src/Bot/Config.fs
Normal file
14
src/Bot/Config.fs
Normal file
@ -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
|
46
src/Bot/Program.fs
Normal file
46
src/Bot/Program.fs
Normal file
@ -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
|
47
src/Bot/WebsiteCheckLoop.fs
Normal file
47
src/Bot/WebsiteCheckLoop.fs
Normal file
@ -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
|
35
src/Bot/WikiMathParser.fs
Normal file
35
src/Bot/WikiMathParser.fs
Normal file
@ -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
Block a user