From f82817d0f8191c699211972d26ad42caeba647df Mon Sep 17 00:00:00 2001 From: h7x4 Date: Fri, 26 Mar 2021 19:39:28 +0100 Subject: [PATCH] Init commit --- .gitignore | 5 ++++ README.md | 20 ++++++++++++++++ WikiMathBot.sln | 39 ++++++++++++++++++++++++++++++ data/channels.dat | 0 src/Bot/Bot.fsproj | 32 +++++++++++++++++++++++++ src/Bot/BotCommands.fs | 28 ++++++++++++++++++++++ src/Bot/Channels.fs | 41 ++++++++++++++++++++++++++++++++ src/Bot/Config.fs | 14 +++++++++++ src/Bot/Program.fs | 46 ++++++++++++++++++++++++++++++++++++ src/Bot/WebsiteCheckLoop.fs | 47 +++++++++++++++++++++++++++++++++++++ src/Bot/WikiMathParser.fs | 35 +++++++++++++++++++++++++++ 11 files changed, 307 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 WikiMathBot.sln create mode 100644 data/channels.dat create mode 100644 src/Bot/Bot.fsproj create mode 100644 src/Bot/BotCommands.fs create mode 100644 src/Bot/Channels.fs create mode 100644 src/Bot/Config.fs create mode 100644 src/Bot/Program.fs create mode 100644 src/Bot/WebsiteCheckLoop.fs create mode 100644 src/Bot/WikiMathParser.fs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5523ab7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +data/config.json + +.ionide +src/Bot/obj +src/Bot/bin \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d386bfa --- /dev/null +++ b/README.md @@ -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": "", + "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///start` + +The project can be run by executing `dotnet run` from within the `src/Bot` directory diff --git a/WikiMathBot.sln b/WikiMathBot.sln new file mode 100644 index 0000000..3b9c30c --- /dev/null +++ b/WikiMathBot.sln @@ -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 diff --git a/data/channels.dat b/data/channels.dat new file mode 100644 index 0000000..e69de29 diff --git a/src/Bot/Bot.fsproj b/src/Bot/Bot.fsproj new file mode 100644 index 0000000..3670eca --- /dev/null +++ b/src/Bot/Bot.fsproj @@ -0,0 +1,32 @@ + + + + true + + + Exe + net5.0 + + + + + + + + + + + + + Always + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bot/BotCommands.fs b/src/Bot/BotCommands.fs new file mode 100644 index 0000000..25c60bc --- /dev/null +++ b/src/Bot/BotCommands.fs @@ -0,0 +1,28 @@ +namespace Bot + +module BotCommands = + + open System.Threading.Tasks + open DSharpPlus.CommandsNext + open DSharpPlus.CommandsNext.Attributes + + open Channels + + type BotCommands() = + [] + member public self.hi(ctx:CommandContext) = + async { ctx.RespondAsync "Hi there" |> ignore } |> Async.StartAsTask :> Task + + [] + member public self.echo(ctx:CommandContext) (message:string) = + async { ctx.RespondAsync message |> ignore } |> Async.StartAsTask :> Task + + [] + 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 \ No newline at end of file diff --git a/src/Bot/Channels.fs b/src/Bot/Channels.fs new file mode 100644 index 0000000..050b754 --- /dev/null +++ b/src/Bot/Channels.fs @@ -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 + + \ No newline at end of file diff --git a/src/Bot/Config.fs b/src/Bot/Config.fs new file mode 100644 index 0000000..b7bd65f --- /dev/null +++ b/src/Bot/Config.fs @@ -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 \ No newline at end of file diff --git a/src/Bot/Program.fs b/src/Bot/Program.fs new file mode 100644 index 0000000..24b5fa4 --- /dev/null +++ b/src/Bot/Program.fs @@ -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() + client.ConnectAsync() |> Async.AwaitTask |> Async.RunSynchronously + do! scrapePeriodically (scrapeFun client previousResults + >> fun results -> previousResults <- results) + do! Async.AwaitTask(Task.Delay(-1)) + } + + [] + let main argv = + Async.RunSynchronously(mainTask) + 0 \ No newline at end of file diff --git a/src/Bot/WebsiteCheckLoop.fs b/src/Bot/WebsiteCheckLoop.fs new file mode 100644 index 0000000..8aa16e0 --- /dev/null +++ b/src/Bot/WebsiteCheckLoop.fs @@ -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) (listOfResults:List) = + 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 \ No newline at end of file diff --git a/src/Bot/WikiMathParser.fs b/src/Bot/WikiMathParser.fs new file mode 100644 index 0000000..dba1e74 --- /dev/null +++ b/src/Bot/WikiMathParser.fs @@ -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 -> unit ) = + timer.Start() + async { + while true do + Async.RunSynchronously waitAPeriod + getStatus + |> callback + }