discord-register 0.0.1 → 0.0.2
raw patch · 10 files changed
+332/−264 lines, 10 filesdep +aesondep +binarydep +bytestringdep −dhalldep −hspecdep −hspec-megaparsecdep ~basedep ~calamitydep ~di
Dependencies added: aeson, binary, bytestring, calamity-commands, data-flags, hashable, hedis, lens, regex-tdfa
Dependencies removed: dhall, hspec, hspec-megaparsec, vector
Dependency ranges changed: base, calamity, di, di-polysemy, generic-lens, megaparsec, polysemy, polysemy-plugin, text, text-show, unboxing-vector, unordered-containers
Files
- ChangeLog.md +1/−0
- README.md +3/−23
- app/Main.hs +35/−1
- config.dhall.empty +0/−20
- discord-register.cabal +104/−69
- src/Kv.hs +36/−0
- src/Lib.hs +94/−137
- src/Parser.hs +38/−0
- src/Utils.hs +21/−0
- test/Spec.hs +0/−14
+ ChangeLog.md view
@@ -0,0 +1,1 @@+Fix issues with intents.
README.md view
@@ -1,25 +1,5 @@ ## Discord-register-### Verification bot for registers with full name and email--Discord-register is an interactive verification bot for registers with full name, email and photo ID requirements.--#### Features:-- All verification forms sent to a verification channel-- Messages configurable with Dhall (except most admin messages in English)-- Verification forms can be reset by those with admin role using a command giving a reason-- Parser for full names, supporting unicode and requiring an underscore if no surname.-- Sane defaults-- Low memory usage (high cpu/memory usage for compile however)-- Written in Haskell using Polysemy with Calamity effects-- No database needed--To build run:-```stack build```--This build depends on `libtinfo-dev` for Ubuntu 20.04. --The configuration is in ```config.dhall```, this needs to be copied and filled in from ```config.dhall.empty```.--Bot tokens can be found in the Discord Developer Portal after creating a bot.+### Verification bot for registers with name and attachment -ID numbers, or Snowflakes, can be found in the UI by right clicking on something after enabling developer mode in Appearance under the settings.+This is a Discord bot for vetting new users, see config.json for an example.+It supports a name query followed by an attachment query.
app/Main.hs view
@@ -1,5 +1,39 @@ module Main where +import Calamity+import Calamity.Cache.InMemory (runCacheInMemory)+import Calamity.Commands (useConstantPrefix)+import Calamity.Commands.Context (useLightContext)+import Calamity.Metrics.Noop (runMetricsNoop)+import Control.Lens+import Control.Monad (void)+import Data.Aeson (eitherDecodeFileStrict)+import Data.ByteString (readFile)+import Data.Flags (allFlags)+import qualified Data.HashMap.Strict as Map+import qualified Di (new)+import DiPolysemy (runDiToIO)+import GHC.Conc (newTVarIO)+import Kv import Lib+import Polysemy+import Polysemy.AtomicState+import Prelude hiding (readFile) -main = execBot+main = do+ eitherDecodeFileStrict "./config.json" >>= \case+ Left e -> print e+ Right cfg -> do+ emptyMap <- newTVarIO (Map.empty :: Map.HashMap (Snowflake User) Form)+ Di.new \di -> -- logging continuation? this is strange+ do+ void . runFinal . embedToFinal . runCacheInMemory+ . runDiToIO di -- logging+ . runMetricsNoop -- no metrics+ . useConstantPrefix "¬"+ . runAtomicStateTVar emptyMap -- trying not to pollute IO+ . runKVStoreAtomic+ . useLightContext -- light command context+ -- all intents+ . runBotIO (BotToken $ cfg ^. #botToken) allFlags+ $ program cfg
− config.dhall.empty
@@ -1,20 +0,0 @@-{ botID.fromSnowflake = 1,- adminID.fromSnowflake = 1,- adminRoleID.fromSnowflake = 1,- vchannelID.fromSnowflake = 1,- botToken = "botToken",- joinMsg =- "Welcome to this organisation. For safe guarding we must check each user is on the society register; for more information please message one of the admin(s) listed in the rules channel. What is your full name?",- faultyNameReciMsg = "Please enter a valid full name, if you have no surname please add an underscore seperated by a space.",- privacyMsg = "Privacy policy",- resetNeedsGuildMsg = "The reset command can only be entered in a guild.",- iveBeenResetMsg = "Your verification form has been reset, what is your full name?",- notAdminMsg = "This command is restricted to admins.",- nameReciMsg = "Thank you, what is your full email?",- emailReciMsg = "Thank you, please could you send photo ID as an attachment (at most 1 file)?",- finishedMsg = "That's all, for more information please type ¬info. For the privacy policy please type ¬info.",- noAtEmailMsg = "This email is invalid, there should be a single at symbol.",- noScreenshotMsg = "This message contains no attachments.",- screenshotReciMsg = "Thank you, that's all. Your information has been sent to a verification channel.",- infoMsg = "This is a bot to verify your full name, email and ID with the organisation's register.",-}
discord-register.cabal view
@@ -1,15 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: cb0ee8652183daa53d0b461b96eb9cfd98ffb2c63c596c26700fe5112264e860 name: discord-register-version: 0.0.1+version: 0.0.2 synopsis: Discord verification bot-description: This bot can send new server members a message on entry, ask for their full name (using a parser to check it is reasonable), email and a picture from them. Reset commands for improper forms are also implemented, as well as help, privacy and info commands. The default prefix is the negation symbol from mathematics.+description: This bot can send new server members a message on entry, ask for their name and an attachment. category: Web homepage: https://github.com/hgsipiere/discord-register#readme bug-reports: https://github.com/hgsipiere/discord-register/issues@@ -18,12 +16,13 @@ copyright: 2020 Hugh Sipiere license: MPL-2.0 license-file: LICENSE-tested-with: GHC == 8.8.4 build-type: Simple+tested-with:+ GHC == 8.10.4 extra-source-files: LICENSE README.md- config.dhall.empty+ ChangeLog.md source-repository head type: git@@ -31,30 +30,63 @@ library exposed-modules:+ Kv Lib+ Parser+ Utils other-modules: Paths_discord_register hs-source-dirs: src- default-extensions: DataKinds FlexibleContexts GADTs LambdaCase PolyKinds RankNTypes ScopedTypeVariables TypeApplications TypeOperators TypeFamilies DeriveGeneric BlockArguments OverloadedLabels OverloadedStrings TemplateHaskell ConstraintKinds DefaultSignatures DeriveFunctor FlexibleInstances MultiParamTypeClasses MultiWayIf UndecidableInstances ViewPatterns NamedFieldPuns- ghc-options: -O2 -fplugin=Polysemy.Plugin+ default-extensions:+ DataKinds+ FlexibleContexts+ GADTs+ LambdaCase+ PolyKinds+ ScopedTypeVariables+ TypeOperators+ TypeFamilies+ DeriveGeneric+ BlockArguments+ OverloadedLabels+ OverloadedStrings+ TemplateHaskell+ ConstraintKinds+ DefaultSignatures+ FlexibleInstances+ MultiParamTypeClasses+ MultiWayIf+ UndecidableInstances+ ViewPatterns+ NamedFieldPuns+ TemplateHaskell+ PartialTypeSignatures+ ConstraintKinds+ LambdaCase+ ghc-options: -fplugin=Polysemy.Plugin build-depends:- base >=4.7 && <5- , calamity >=0.1.20.1 && <0.1.21- , dhall >=1.35.0 && <1.36.1- , di >=1.3 && <1.3.1- , di-polysemy >=0.2.0.0 && <0.2.1- , generic-lens >=2.0.0.0 && <2.0.1- , hspec >=2.7.4 && <2.7.5- , hspec-megaparsec >=2.1.0 && <2.1.1- , megaparsec >=8.0.0- , polysemy >=1.3.0.0 && <1.3.1- , polysemy-plugin >=0.2.5.0 && <0.2.6- , text >=1.2.4.0 && <1.2.5- , text-show >=3.8.5 && <3.9.1- , unboxing-vector >=0.1.1.0- , unordered-containers >=0.2.10.0 && <0.2.13.0- , vector >=0.12.1.2 && <0.12.2+ aeson+ , base ==4.14.*+ , binary+ , bytestring+ , calamity+ , calamity-commands+ , data-flags+ , di+ , di-polysemy+ , generic-lens+ , hashable+ , hedis+ , lens+ , megaparsec+ , polysemy+ , polysemy-plugin+ , regex-tdfa+ , text+ , text-show+ , unboxing-vector+ , unordered-containers default-language: Haskell2010 executable discord-register-exe@@ -63,50 +95,53 @@ Paths_discord_register hs-source-dirs: app- build-depends:- base >=4.7 && <5- , calamity >=0.1.20.1 && <0.1.21- , dhall >=1.35.0 && <1.36.1- , di >=1.3 && <1.3.1- , di-polysemy >=0.2.0.0 && <0.2.1- , discord-register- , generic-lens >=2.0.0.0 && <2.0.1- , hspec >=2.7.4 && <2.7.5- , hspec-megaparsec >=2.1.0 && <2.1.1- , megaparsec >=8.0.0- , polysemy >=1.3.0.0 && <1.3.1- , polysemy-plugin >=0.2.5.0 && <0.2.6- , text >=1.2.4.0 && <1.2.5- , text-show >=3.8.5 && <3.9.1- , unboxing-vector >=0.1.1.0- , unordered-containers >=0.2.10.0 && <0.2.13.0- , vector >=0.12.1.2 && <0.12.2- default-language: Haskell2010--test-suite discord-register-test- type: exitcode-stdio-1.0- main-is: Spec.hs- other-modules:- Paths_discord_register- hs-source-dirs:- test- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-extensions:+ DataKinds+ FlexibleContexts+ GADTs+ LambdaCase+ PolyKinds+ ScopedTypeVariables+ TypeOperators+ TypeFamilies+ DeriveGeneric+ BlockArguments+ OverloadedLabels+ OverloadedStrings+ TemplateHaskell+ ConstraintKinds+ DefaultSignatures+ FlexibleInstances+ MultiParamTypeClasses+ MultiWayIf+ UndecidableInstances+ ViewPatterns+ NamedFieldPuns+ TemplateHaskell+ PartialTypeSignatures+ ConstraintKinds+ LambdaCase build-depends:- base >=4.7 && <5- , calamity >=0.1.20.1 && <0.1.21- , dhall >=1.35.0 && <1.36.1- , di >=1.3 && <1.3.1- , di-polysemy >=0.2.0.0 && <0.2.1+ aeson+ , base ==4.14.*+ , binary+ , bytestring+ , calamity+ , calamity-commands+ , data-flags+ , di+ , di-polysemy , discord-register- , generic-lens >=2.0.0.0 && <2.0.1- , hspec >=2.7.4 && <2.7.5- , hspec-megaparsec >=2.1.0 && <2.1.1- , megaparsec >=8.0.0- , polysemy >=1.3.0.0 && <1.3.1- , polysemy-plugin >=0.2.5.0 && <0.2.6- , text >=1.2.4.0 && <1.2.5- , text-show >=3.8.5 && <3.9.1- , unboxing-vector >=0.1.1.0- , unordered-containers >=0.2.10.0 && <0.2.13.0- , vector >=0.12.1.2 && <0.12.2+ , generic-lens+ , hashable+ , hedis+ , lens+ , megaparsec+ , polysemy+ , polysemy-plugin+ , regex-tdfa+ , text+ , text-show+ , unboxing-vector+ , unordered-containers default-language: Haskell2010
+ src/Kv.hs view
@@ -0,0 +1,36 @@+module Kv where++import Control.Monad+import Data.Either+import Data.HashMap.Strict as Map+import Data.Hashable (Hashable)+import Data.Maybe+import Polysemy+import Polysemy.AtomicState++data KVStore k v m a where+ Kvset :: k -> v -> KVStore k v m ()+ Kvget :: k -> KVStore k v m (Maybe v)+ Kvdel :: k -> KVStore k v m ()++makeSem ''KVStore -- template haskell++-- we use atomic operations to be thread safe in concurrency+runKVStoreAtomic ::+ ( Member (AtomicState (HashMap k v)) r,+ Eq k,+ Hashable k+ ) =>+ Sem (KVStore k v ': r) a ->+ Sem r a+runKVStoreAtomic = interpret $ \case+ Kvset k v -> atomicModify' $ Map.insert k v+ Kvget k -> atomicGets $ Map.lookup k+ Kvdel k -> atomicModify' $ Map.delete k++kvgets ::+ (Member (KVStore k v) r, Eq k, Hashable k) =>+ (v -> a) ->+ k ->+ Sem r (Maybe a)+kvgets f k = fmap f <$> kvget k
src/Lib.hs view
@@ -1,156 +1,113 @@+{-# LANGUAGE TypeApplications #-}+ module Lib where -import Calamity hiding (author)-import Calamity.Cache.InMemory-import Calamity.Commands-import Calamity.Metrics.Noop+import Calamity as Cal hiding (Embed, Member, author, embed)+import Calamity.Commands (addCommands, command, helpCommand)+import Calamity.Commands.Context (ctxUserID)+import qualified Calamity.HTTP.Channel as HttpChan import qualified Calamity.HTTP.Guild-import Calamity.Types.Model.Channel.Message as M--import Data.Generics.Internal.VL ((^.))-import Control.Monad (void)--import qualified Data.HashMap.Strict as Map (insert, empty, lookup, HashMap)-import Data.Maybe (isNothing)-import qualified Data.Text as T-import Data.Text.Lazy as TL (Text, pack, intercalate, unpack, foldl', append, any, singleton)--import qualified Dhall (input, auto, FromDhall)--import qualified Di (new)-import DiPolysemy (runDiToIO, info)--import GHC.Generics--import qualified Polysemy as P-import qualified Polysemy.AtomicState as P--import TextShow--import Text.Megaparsec.Byte (space, string)-+import qualified Calamity.Types.Model.Channel.Attachment as Attachm+import Calamity.Types.Model.Channel.Message as Messg+import Calamity.Types.Upgradeable (upgrade)+import Control.Lens+import Control.Monad (mapM_, void)+import Data.Aeson (FromJSON, ToJSON)+import Data.List (foldl')+import Data.Maybe (isJust)+import qualified Data.Text as TS+import Data.Text.Lazy as TL (Text, any, append, intercalate, pack, unpack, unwords)+import Data.Text.Lazy.IO (writeFile) import qualified Data.Vector.Unboxing as V (elem)+import GHC.Generics (Generic)+import Kv+import Parser (parseFullName) -- get rid of this+import Polysemy+import Text.Regex.TDFA ((=~))+import Utils (EssentialEnv)+import Prelude hiding (any, intercalate, readFile, unwords, writeFile) -import Data.Void+data InfoCmd = InfoCmd {cmd_name :: TS.Text, info :: Text} deriving (Generic) -import Text.Megaparsec-import Text.Megaparsec.Char as C-import qualified Text.Megaparsec.Char.Lexer as CL+data Query = Query {incorrect_response :: Text, correct_response :: Text} deriving (Generic) --- similar to mccarthy conditional-if' True a _ = a-if' False _ b = b+data Cfg = Cfg+ { botToken :: Text,+ botID :: Snowflake User,+ vChannelID :: Snowflake Channel,+ joinMsg :: Text,+ infoCommands :: [InfoCmd],+ name_query :: Query,+ screenshot_query :: Query,+ done :: Text+ }+ deriving (Generic) --- parser for full name, the underscore--- is to handle the edge case of a single name, full name-type ParserStr = Parsec Void String+-- this allows us to use a configuration file+instance FromJSON Query -preLexeme, lexeme :: ParserStr a -> ParserStr a-lexeme = CL.lexeme C.space-preLexeme = (*>) C.space+instance FromJSON InfoCmd -nameOrUnderscore', fullName :: ParserStr String-nameOrUnderscore' = (some letterChar) <|> (fmap pure . char $ '_')-fullName = do- firstName <- preLexeme nameOrUnderscore'- restNames <- lexeme . some . preLexeme $ nameOrUnderscore'- pure $ foldl (\z s -> z ++ " " ++ s) firstName restNames+instance FromJSON Cfg -data Settings = Settings {- botToken :: Text, botID :: Snowflake User, adminID :: Snowflake User, vchannelID :: Snowflake Channel,- adminRoleID :: Snowflake Role,- joinMsg :: Text, faultyNameReciMsg :: Text, nameReciMsg :: Text, emailReciMsg :: Text, screenshotReciMsg :: Text,- noAtEmailMsg :: Text, noScreenshotMsg :: Text, finishedMsg :: Text,- privacyMsg :: Text, infoMsg :: Text,- iveBeenResetMsg :: Text, resetNeedsGuildMsg :: Text, notAdminMsg :: Text-} deriving (Generic, Show)+data Form = NoDetails | Named Text | Finished -instance Dhall.FromDhall (Snowflake a)-instance Dhall.FromDhall Settings-data Form = Empty | Named Text | NamedEmailed Text Text | Finished+-- proposition that a value is a program with key value store+type Env r c =+ ( EssentialEnv r c,+ Member (KVStore (Snowflake User) Form) r+ ) -mentionUser :: Snowflake User -> Text -> Text -> CreateMessageOptions-mentionUser sUser preMention content = CreateMessageOptions- (Just . T.pack . TL.unpack $- -- discord lets you type <@uid> to mention someone where uid is their discord user id- preMention `append` "<@" `append` showtl sUser `append` ">" `append` content)- Nothing Nothing Nothing Nothing -- no nonce, tts, file, embed- (Just (AllowedMentions [AllowedMentionUsers] [] [sUser]))+messageCreateAction :: Env r c => Cfg -> Message -> Sem r ()+messageCreateAction cfg msg@Message {author, Messg.content = response, attachments} =+ -- check the bot would be replying to itself or a non-personal message+ -- this should also avoid replying to a command too, todo later+ if author == cfg ^. #botID || isJust (msg ^. #guildID)+ then pure ()+ else+ kvget author >>= \case+ Nothing -> grabName cfg author response -- nothing stored about them+ Just NoDetails -> grabName cfg author response -- they gave no details+ Just (Named name) -> grabScreenshot cfg author name attachments+ Just Finished -> void . tell author $ cfg ^. #done -grabName cfg msg = - case runParser fullName "" (unpack response) of- -- we don't give back parser error messages because they don't tell how to terminate a parse- -- but rather how to let the combinators continue, this is very confusing- Left error -> void $ tell author $ cfg ^. #faultyNameReciMsg+grabName :: Env r c => Cfg -> Snowflake User -> Text -> Sem r ()+-- this should parse inline so we can remove megaparsec as dependency+grabName cfg author response = case parseFullName response of+ Left _ -> void $ tell author $ cfg ^. #name_query ^. #incorrect_response Right name -> do- current <- P.atomicGet- _ <- P.atomicPut $ Map.insert author (Named $ showtl name) current- void $ tell author $ cfg ^. #nameReciMsg- where author = msg ^. #author- response = msg ^. #content--grabEmail cfg name msg =- case (TL.foldl' (\n c -> if' (c == '@') (n+1) n) 0 response) == 1 of- False -> void $ tell author $ cfg ^. #noAtEmailMsg- True -> do- current <- P.atomicGet- _ <- P.atomicPut $ Map.insert author (NamedEmailed name response) current- void $ tell author $ cfg ^. #emailReciMsg- where response = msg ^. #content- author = msg ^. #author--grabScreenshot cfg name email msg =- let author = msg ^. #author in- case msg ^. #attachments of- files@(x:_) -> do- current <- P.atomicGet- _ <- P.atomicPut $ Map.insert author Finished current- void . tell (cfg ^. #vchannelID) . mentionUser author "Discord ID: " $- -- library bug means we don't embed the picture, instead a URL- "\nName: " <> name <> "\nEmail: " <> email <> "\nAttachment: " <> (x ^. #url)- void $ tell @Text author $ cfg ^. #screenshotReciMsg- [] -> void $ tell @Text author $ cfg ^. #noScreenshotMsg---- the bot responding to its messages counts as an event, so ignore that to prevent looping-messageCreateAction cfg = \msg@Message{author, M.content = response} -> do- case author /= cfg ^. #botID && isNothing (msg ^. #guildID) && (not $ TL.any (== '¬') response) of- False -> pure ()- True -> do- current <- P.atomicGet- case Map.lookup author current of- Nothing -> grabName cfg msg- Just Empty -> grabName cfg msg- Just (Named name) -> grabEmail cfg name msg- Just (NamedEmailed name email) -> grabScreenshot cfg name email msg- Just Finished -> void $ tell author $ cfg ^. #finishedMsg+ kvset author (Named name)+ void $ tell author $ cfg ^. #name_query ^. #correct_response -resetCommand cfg = command @'[Snowflake User, [Text]] "reset" \ctx sUser reason ->- case ctx ^. #member of- -- more intentional and simpler to do in a server channel so other people can see- Nothing -> void $ tell @Text ctx $ cfg ^. #resetNeedsGuildMsg- Just mem -> if V.elem (cfg ^. #adminRoleID) (mem ^. #roles) then- do- current <- P.atomicGet- _ <- P.atomicPut $ Map.insert sUser Empty current- void $ tell @Text sUser $ TL.intercalate " " reason- void $ tell sUser $ cfg ^. #iveBeenResetMsg- else void $ tell ctx $ cfg ^. #notAdminMsg+grabScreenshot ::+ Env r c =>+ Cfg ->+ Snowflake User ->+ Text ->+ [Attachm.Attachment] ->+ Sem r ()+grabScreenshot cfg author _ [] =+ void . tell author $ cfg ^. #screenshot_query ^. #incorrect_response+grabScreenshot cfg author name (file : _) = do+ kvset author Finished+ tell author $ cfg ^. #screenshot_query ^. #correct_response+ void . tell (cfg ^. #vChannelID) $+ "Discord ID: " <> mention author <> "\n"+ <> "Name: "+ <> name+ <> "\n"+ <> "Attachment: "+ <> file ^. #url -commandsAdded cfg = addCommands $ do+addedCommands :: Env r c => Cfg -> Sem r ()+addedCommands cfg = void . addCommands $ do helpCommand- command @'[] "info" \ctx -> void $ tell ctx $ cfg ^. #infoMsg- command @'[] "privacy" \ctx -> void $ tell ctx $ cfg ^. #privacyMsg- resetCommand cfg--program cfg = - (react @'GuildMemberAddEvt \ctx -> void $ tell ctx $ cfg ^. #joinMsg) >>- (react @'MessageCreateEvt $ messageCreateAction cfg) >>- commandsAdded cfg+ mapM_ -- add all the information commands, e.g. info, privacy etc...+ (\(InfoCmd cmdName info) -> command @'[] cmdName $ \ctx -> void $ tell ctx info)+ $ cfg ^. #infoCommands -execBot :: IO ()-execBot = do- cfg <- Dhall.input Dhall.auto "./config.dhall" :: IO Settings- Di.new \di -> do- void . P.runFinal . P.embedToFinal. runCacheInMemory . runDiToIO di. runMetricsNoop. useConstantPrefix "¬" .- -- if run without IO, the state is localised, ignore final state via fmap- fmap snd . P.atomicStateToIO Map.empty . runBotIO (BotToken $ cfg ^. #botToken) $ program cfg+program :: Env r c => Cfg -> Sem r ()+program cfg = do+ addedCommands cfg+ react @'GuildMemberAddEvt $ \ctx -> void . tell ctx $ cfg ^. #joinMsg+ void . react @'MessageCreateEvt $ messageCreateAction cfg
+ src/Parser.hs view
@@ -0,0 +1,38 @@+module Parser where++-- i think this whole file can be replaced+-- with about 12 characters of code less than+-- a line with a regular expression+-- the benefit of a parser's error messages+-- we never use because, interpreting parser combinator+-- error messages is not useful as the error+-- messages are why it can't continue not why+-- it doesnt end parsing++import Data.List (intercalate)+import qualified Data.Text.Lazy as TL (Text, unpack)+import Data.Void (Void)+import Text.Megaparsec (Parsec, runParser, some, (<|>))+import Text.Megaparsec.Byte (space, string)+import Text.Megaparsec.Char as C (char, letterChar, space)+import qualified Text.Megaparsec.Char.Lexer as CL+import TextShow (showtl)++--import Db++-- parser for full name, the underscore+-- is to handle the edge case of a single name, full name+type ParserStr = Parsec Void String++preLexeme, lexeme :: ParserStr a -> ParserStr a+lexeme = CL.lexeme C.space+preLexeme = (*>) C.space++nameOrUnderscore', fullName :: ParserStr String+nameOrUnderscore' = some letterChar <|> (fmap pure . char $ '_')+fullName = do+ firstName <- preLexeme nameOrUnderscore'+ restNames <- lexeme . some . preLexeme $ nameOrUnderscore'+ pure . intercalate " " $ firstName : restNames++parseFullName content = showtl <$> (runParser fullName "" (TL.unpack content))
+ src/Utils.hs view
@@ -0,0 +1,21 @@+-- this file provides utilities such as type aliases++module Utils where++import Calamity+import Calamity.Commands.Context (CalamityCommandContext)+import Calamity.Commands.Types (CommandContext)+import Calamity.Types.Model.Channel.Message (Message)+import CalamityCommands.Context (ConstructContext)+import CalamityCommands.ParsePrefix (ParsePrefix)+import Data.Typeable (Typeable)+import Polysemy (Members)++type EssentialEnv r c =+ ( BotC r,+ Typeable c,+ Tellable c,+ CommandContext c,+ CalamityCommandContext c,+ Members '[ParsePrefix Message, ConstructContext Message c IO ()] r+ )
− test/Spec.hs
@@ -1,14 +0,0 @@-import Test.Hspec-import Test.Hspec.Megaparsec--import Text.Megaparsec--import Lib--main = hspec.parallel $ describe "parsing" $ do- it "parse K as name or underscore" (parse nameOrUnderscore' "" "K" `shouldParse` "K")- it "attempt to parse 2 as name or underscore" (parse nameOrUnderscore' "" `shouldFailOn` "2")- it "parse _ as name or underscore" (parse nameOrUnderscore' "" "_" `shouldParse` "_")- it "attempt to parse Kelly as full name" (parse fullName "" `shouldFailOn` "Kelly")- it "parse Kelly Bloggs as full name" (parse fullName "" "Kelly Bloggs" `shouldParse` "Kelly Bloggs")- it "parse Kelly _ as full name" (parse fullName "" "Kelly _" `shouldParse` "Kelly _")