Init commit

master
Oystein Kristoffer Tveit 2021-03-26 19:39:28 +01:00
commit f82817d0f8
11 changed files with 307 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
data/config.json
.ionide
src/Bot/obj
src/Bot/bin

20
README.md Normal file
View 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
View 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
View File

32
src/Bot/Bot.fsproj Normal file
View 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
View 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
View 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
View 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
View 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

View 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
View 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
}