aboutsummaryrefslogtreecommitdiff
path: root/tea-cleaner
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2026-06-29 15:10:26 +0200
committerEugen Wissner <belka@caraus.de>2026-06-29 15:10:26 +0200
commit5cb4767698cf5b15906fad1d43ac0cfb5ec76ff4 (patch)
tree39038f6777ad9adebca0a9e0ba5a19843e8760be /tea-cleaner
parent620038356a50f30459820833fdf3a38250c6cbf9 (diff)
downloadkazbek-master.tar.gz
Remove tea-cleanerHEADmaster
Since I do not use gitea anymore and cannot maintain the compatibility with the new versions.
Diffstat (limited to 'tea-cleaner')
-rw-r--r--tea-cleaner/Main.hs81
-rw-r--r--tea-cleaner/TeaCleaner/Client.hs141
-rw-r--r--tea-cleaner/TeaCleaner/Configuration.hs87
-rw-r--r--tea-cleaner/TeaCleaner/Filter.hs64
-rw-r--r--tea-cleaner/TeaCleaner/Options.hs14
5 files changed, 0 insertions, 387 deletions
diff --git a/tea-cleaner/Main.hs b/tea-cleaner/Main.hs
deleted file mode 100644
index e8eea68..0000000
--- a/tea-cleaner/Main.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-{- This Source Code Form is subject to the terms of the Mozilla Public License,
- v. 2.0. If a copy of the MPL was not distributed with this file, You can
- obtain one at https://mozilla.org/MPL/2.0/. -}
-
-module Main
- ( main
- ) where
-
-import Data.Vector (Vector)
-import qualified Data.Text as Text
-import qualified Data.Vector as Vector
-import qualified Data.Text.IO as Text.IO
-import qualified Data.Text.Lazy.IO as Text.Lazy.IO
-import qualified Data.Text.Lazy.Builder as Text.Builder
-import qualified Data.Text.Lazy.Builder.Int as Text.Builder
-import GHC.Records (HasField(..))
-import TeaCleaner.Filter
- ( UserFilter(..)
- , FilterResult(..)
- , filterByActivities
- , filterByUserProperties
- )
-import TeaCleaner.Client (User(..), getUsers, purgeUser)
-import TeaCleaner.Configuration
- ( ProgramOptions(..)
- , Settings(..)
- , decodeSettingsFile
- , commandLineInfo
- , execParser
- )
-import Control.Monad (when)
-import Data.IORef (modifyIORef, readIORef)
-
-printStatistics :: Settings -> IO ()
-printStatistics Settings{ statistics } =
- readIORef statistics >>= printCount
- where
- printCount count =
- let count' = Text.Builder.decimal count
- in Text.Lazy.IO.putStrLn
- $ Text.Builder.toLazyText
- $ "Count: " <> count'
-
-handleResults :: Settings -> Bool -> Vector FilterResult -> IO (Vector User)
-handleResults settings live =
- Vector.foldM' handleResult Vector.empty
- where
- handleResult accumulator (FilterResult user FailedFilter)
- = handleFailedFilter settings live user
- >> pure accumulator
- handleResult accumulator (FilterResult _ PassFilter) = pure accumulator
- handleResult accumulator (FilterResult user SuspiciousFilter) = pure
- $ Vector.snoc accumulator user
-
-handleFailedFilter :: Settings -> Bool -> User -> IO ()
-handleFailedFilter settings live user = Text.IO.putStrLn buildValue
- >> modifyIORef (getField @"statistics" settings) (+ 1)
- >> when live (purgeUser settings user)
- where
- buildValue = getField @"username" user <> "\n"
- <> " Website: " <> getField @"website" user <> "\n"
- <> " Created: " <> Text.pack (show $ getField @"created" user) <> "\n"
- <> " Last login: " <> Text.pack (show $ getField @"lastLogin" user) <> "\n"
- <> " Email: " <> getField @"email" user <> "\n"
- <> " Website: " <> getField @"website" user <> "\n"
- <> " Description: " <> getField @"description" user <> "\n"
- <> " Avatar: " <> getField @"avatarUrl" user <> "\n"
- <> "\n"
-
-main :: IO ()
-main = execParser commandLineInfo >>= withArguments
- where
- withArguments ProgramOptions{ live } = decodeSettingsFile "config/tea-cleaner.toml"
- >>= withSettings live
-
-withSettings :: Bool -> Settings -> IO ()
-withSettings live settings = getUsers settings
- >>= handleResults settings live . fmap (filterByUserProperties settings)
- >>= traverse (filterByActivities settings)
- >>= handleResults settings live
- >> printStatistics settings
diff --git a/tea-cleaner/TeaCleaner/Client.hs b/tea-cleaner/TeaCleaner/Client.hs
deleted file mode 100644
index fae3e6a..0000000
--- a/tea-cleaner/TeaCleaner/Client.hs
+++ /dev/null
@@ -1,141 +0,0 @@
-{- This Source Code Form is subject to the terms of the Mozilla Public License,
- v. 2.0. If a copy of the MPL was not distributed with this file, You can
- obtain one at https://mozilla.org/MPL/2.0/. -}
-
-module TeaCleaner.Client
- ( Activity(..)
- , User(..)
- , getActivities
- , getUsers
- , purgeUser
- ) where
-
-import Data.List.NonEmpty (NonEmpty(..))
-import Data.Text (StrictText)
-import qualified Data.Text.Encoding as Text.Encoding
-import Data.Vector (Vector)
-import qualified Text.URI as URI
-import qualified Text.URI.QQ as URI
-import Network.HTTP.Req
- ( DELETE(..)
- , GET(..)
- , NoReqBody(..)
- , HttpMethod(..)
- , HttpBody
- , HttpResponse(..)
- , HttpBodyAllowed
- , ProvidesBody
- , defaultHttpConfig
- , ignoreResponse
- , jsonResponse
- , oAuth2Bearer
- , responseBody
- , req
- , runReq
- , useHttpsURI
- )
-import GHC.Records (HasField(..))
-import TeaCleaner.Options (jsonOptions)
-import Data.Int (Int64)
-import qualified Data.Aeson.TH as Aeson
-import Data.Time (ZonedTime(..))
-import TeaCleaner.Configuration (Settings(..))
-import Data.Data (Proxy)
-import Text.URI (URI)
-
-data User = User
- { id :: Int64
- , login :: StrictText
- , loginName :: StrictText
- , fullName :: StrictText
- , email :: StrictText
- , avatarUrl :: StrictText
- , language :: StrictText
- , isAdmin :: Bool
- , lastLogin :: ZonedTime
- , created :: ZonedTime
- , restricted :: Bool
- , active :: Bool
- , prohibitLogin :: Bool
- , location :: StrictText
- , website :: StrictText
- , description :: StrictText
- , visibility :: StrictText
- , followersCount :: Int
- , followingCount :: Int
- , starredReposCount :: Int
- , username :: StrictText
- } deriving (Show)
-
-$(Aeson.deriveJSON jsonOptions ''User)
-
-data Activity = Activity
- { actUserId :: Int64
- -- , comment Comment
- , commentId :: Int64
- , content :: StrictText
- , created :: ZonedTime
- , id :: Int64
- , isPrivate :: Bool
- , opType :: StrictText
- , refName :: StrictText
- -- repo Repository{...}
- , repoId :: Int64
- , userId :: Int64
- } deriving (Show)
-
-$(Aeson.deriveJSON jsonOptions ''Activity)
-
-purgeUser :: Settings -> User -> IO ()
-purgeUser Settings{..} user =
- let pathConstructor lastPiece = [URI.pathPiece|api|] :|
- [ [URI.pathPiece|v1|]
- , [URI.pathPiece|admin|]
- , [URI.pathPiece|users|]
- , lastPiece
- ]
- uri = server
- { URI.uriPath = (False,) . pathConstructor
- <$> URI.mkPathPiece (getField @"username" user)
- , URI.uriQuery = [URI.QueryParam [URI.queryKey|purge|] [URI.queryValue|true|]]
- }
- in doRequest uri token DELETE NoReqBody ignoreResponse
-
-getActivities :: Settings -> User -> IO (Vector Activity)
-getActivities Settings{..} user =
- let pathConstructor lastPiece = [URI.pathPiece|api|] :|
- [ [URI.pathPiece|v1|]
- , [URI.pathPiece|users|]
- , lastPiece
- , [URI.pathPiece|activities|]
- , [URI.pathPiece|feeds|]
- ]
- uri = server
- { URI.uriPath = (False,) . pathConstructor
- <$> URI.mkPathPiece (getField @"username" user)
- , URI.uriQuery = [URI.QueryParam [URI.queryKey|purge|] [URI.queryValue|true|]]
- }
- in doRequest uri token GET NoReqBody jsonResponse
-
-getUsers :: Settings -> IO (Vector User)
-getUsers Settings{..} =
- let pathPieces = [URI.pathPiece|api|] :|
- [ [URI.pathPiece|v1|]
- , [URI.pathPiece|admin|]
- , [URI.pathPiece|users|]
- ]
- uri = server
- { URI.uriPath = Just (False, pathPieces)
- }
- in doRequest uri token GET NoReqBody jsonResponse
-
-doRequest
- :: (HttpMethod method, HttpBody body, HttpResponse response, HttpBodyAllowed (AllowsBody method) (ProvidesBody body))
- => URI -> StrictText -> method -> body -> Proxy response -> IO (HttpResponseBody response)
-doRequest uri token method body response =
- case useHttpsURI uri of
- Just (httpsURI, httpsOptions) -> fmap responseBody
- $ runReq defaultHttpConfig
- $ req method httpsURI body response
- $ httpsOptions <> oAuth2Bearer (Text.Encoding.encodeUtf8 token)
- Nothing -> error "Invalid https URI"
diff --git a/tea-cleaner/TeaCleaner/Configuration.hs b/tea-cleaner/TeaCleaner/Configuration.hs
deleted file mode 100644
index b958264..0000000
--- a/tea-cleaner/TeaCleaner/Configuration.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-{- This Source Code Form is subject to the terms of the Mozilla Public License,
- v. 2.0. If a copy of the MPL was not distributed with this file, You can
- obtain one at https://mozilla.org/MPL/2.0/. -}
-
-module TeaCleaner.Configuration
- ( ProgramOptions(..)
- , Settings(..)
- , decodeSettingsFile
- , commandLineInfo
- , execParser
- ) where
-
-import GHC.Records (HasField(..))
-import Data.Text (StrictText)
-import qualified Toml
-import Toml ((.=))
-import Options.Applicative
- ( Parser
- , ParserInfo
- , (<**>)
- , execParser
- , fullDesc
- , help
- , helper
- , info
- , long
- , progDesc
- , switch
- )
-import Text.URI (URI)
-import qualified Text.URI as URI
-import Data.Time (UTCTime(..), getCurrentTime)
-import Data.IORef (IORef, newIORef)
-
-data ConfigFile = ConfigFile
- { token :: StrictText
- , server :: StrictText
- , spamWords :: [StrictText]
- , mailDomains :: [StrictText]
- , noLogin :: Word
- } deriving (Eq, Show)
-
-configFileCodec :: Toml.TomlCodec ConfigFile
-configFileCodec = ConfigFile
- <$> Toml.text "token" .= getField @"token"
- <*> Toml.text "server" .= getField @"server"
- <*> Toml.arrayOf Toml._Text "spam_words" .= getField @"spamWords"
- <*> Toml.arrayOf Toml._Text "mail_domains" .= getField @"mailDomains"
- <*> Toml.word "no_login" .= getField @"noLogin"
-
-data Settings = Settings
- { token :: StrictText
- , server :: URI
- , now :: UTCTime
- , spamWords :: [StrictText]
- , mailDomains :: [StrictText]
- , statistics :: IORef Int
- , noLogin :: Word
- } deriving Eq
-
-decodeSettingsFile :: FilePath -> IO Settings
-decodeSettingsFile configPath = do
- ConfigFile{..} <- Toml.decodeFile configFileCodec configPath
- parsedServer <- URI.mkURI server
- now <- getCurrentTime
- ioRef <- newIORef 0
- pure $ Settings
- { token = token
- , server = parsedServer
- , now = now
- , spamWords = spamWords
- , mailDomains = mailDomains
- , noLogin = noLogin
- , statistics = ioRef
- }
-
-newtype ProgramOptions = ProgramOptions
- { live :: Bool
- } deriving (Eq, Show)
-
-commandLineInfo :: ParserInfo ProgramOptions
-commandLineInfo = info (commandLine <**> helper)
- $ fullDesc <> progDesc "Helps to detect some spam gitea accounts"
-
-commandLine :: Parser ProgramOptions
-commandLine = ProgramOptions
- <$> switch (long "live" <> help "Purge suspicious users")
diff --git a/tea-cleaner/TeaCleaner/Filter.hs b/tea-cleaner/TeaCleaner/Filter.hs
deleted file mode 100644
index f7d3315..0000000
--- a/tea-cleaner/TeaCleaner/Filter.hs
+++ /dev/null
@@ -1,64 +0,0 @@
-{- This Source Code Form is subject to the terms of the Mozilla Public License,
- v. 2.0. If a copy of the MPL was not distributed with this file, You can
- obtain one at https://mozilla.org/MPL/2.0/. -}
-
-module TeaCleaner.Filter
- ( FilterResult(..)
- , UserFilter(..)
- , filterByActivities
- , filterByUserProperties
- ) where
-
-import qualified Data.Text as Text
-import Data.Time (LocalTime(..), ZonedTime(..), UTCTime(..), addUTCTime)
-import qualified Data.Vector as Vector
-import TeaCleaner.Client (Activity(..), User(..), getActivities)
-import TeaCleaner.Configuration (Settings(..))
-import GHC.Records (HasField(..))
-
-data UserFilter
- = PassFilter
- | SuspiciousFilter
- | FailedFilter
- deriving (Eq, Show)
-
-data FilterResult = FilterResult User UserFilter
- deriving (Show)
-
-filterByUserProperties :: Settings -> User -> FilterResult
-filterByUserProperties settings user@User{ created, lastLogin }
- | noLoginSinceRegistration = FilterResult user FailedFilter
- | containsSpamWords = FilterResult user FailedFilter
- | percentEncodedWebsite = FilterResult user FailedFilter
- | hasFullDescription = FilterResult user SuspiciousFilter
- | unusualMailDomains = FilterResult user SuspiciousFilter
- | otherwise = FilterResult user PassFilter
- where
- percentEncodedWebsite = Text.elem '%' $ getField @"website" user
- unusualMailDomains =
- let predicate = (`Text.isSuffixOf` getField @"email" user)
- in any predicate (getField @"mailDomains" settings)
- containsSpamWords =
- let lowerCaseDescription = Text.toLower $ getField @"description" user
- lowerCaseWebsite = Text.toLower $ getField @"website" user
- predicate word = Text.isInfixOf word lowerCaseWebsite
- || Text.isInfixOf word lowerCaseDescription
- in any predicate (getField @"spamWords" settings)
- hasFullDescription
- = not (Text.null $ getField @"description" user)
- && not (Text.null $ getField @"website" user)
- noLoginSinceRegistration =
- let period = fromIntegral (getField @"noLogin" settings) * (-3600) * 24
- periodAgo = utctDay $ addUTCTime period $ getField @"now" settings
- in zonedDay created < periodAgo && zonedDay created == zonedDay lastLogin
- zonedDay = localDay . zonedTimeToLocalTime
-
-filterByActivities :: Settings -> User -> IO FilterResult
-filterByActivities settings user = getActivities settings user
- >>= evalActivities
- where
- evalActivities activities
- | Just (Activity{ opType }, rest) <- Vector.uncons activities
- , Vector.null rest
- , opType == "create_repo" = pure $ FilterResult user FailedFilter
- evalActivities _ = pure $ FilterResult user PassFilter
diff --git a/tea-cleaner/TeaCleaner/Options.hs b/tea-cleaner/TeaCleaner/Options.hs
deleted file mode 100644
index b1b6372..0000000
--- a/tea-cleaner/TeaCleaner/Options.hs
+++ /dev/null
@@ -1,14 +0,0 @@
-{- This Source Code Form is subject to the terms of the Mozilla Public License,
- v. 2.0. If a copy of the MPL was not distributed with this file, You can
- obtain one at https://mozilla.org/MPL/2.0/. -}
-
-module TeaCleaner.Options
- ( jsonOptions
- ) where
-
-import qualified Data.Aeson as Aeson
-
-jsonOptions :: Aeson.Options
-jsonOptions = Aeson.defaultOptions
- { Aeson.fieldLabelModifier = Aeson.camelTo2 '_'
- }