packages feed

poke-0.0.0.1: src/Lib.hs

module Lib where

import Calamity hiding (author)
import Calamity.Cache.InMemory
import Calamity.Commands
import Calamity.Metrics.Noop
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 Data.Vector.Unboxing as V (elem)

import Data.Void

import Text.Megaparsec
import Text.Megaparsec.Char as C
import qualified Text.Megaparsec.Char.Lexer as CL

-- similar to mccarthy conditional
if' True a _ = a
if' False _ b = b

-- 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 $ foldl (\z s -> z ++ " " ++ s) firstName restNames

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)

instance Dhall.FromDhall (Snowflake a)
instance Dhall.FromDhall Settings
data Form = Empty | Named Text | NamedEmailed Text Text | Finished

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]))

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

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

commandsAdded cfg = 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

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