From 5cb4767698cf5b15906fad1d43ac0cfb5ec76ff4 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Mon, 29 Jun 2026 15:10:26 +0200 Subject: Remove tea-cleaner Since I do not use gitea anymore and cannot maintain the compatibility with the new versions. --- tea-cleaner/Main.hs | 81 ------------------ tea-cleaner/TeaCleaner/Client.hs | 141 -------------------------------- tea-cleaner/TeaCleaner/Configuration.hs | 87 -------------------- tea-cleaner/TeaCleaner/Filter.hs | 64 --------------- tea-cleaner/TeaCleaner/Options.hs | 14 ---- 5 files changed, 387 deletions(-) delete mode 100644 tea-cleaner/Main.hs delete mode 100644 tea-cleaner/TeaCleaner/Client.hs delete mode 100644 tea-cleaner/TeaCleaner/Configuration.hs delete mode 100644 tea-cleaner/TeaCleaner/Filter.hs delete mode 100644 tea-cleaner/TeaCleaner/Options.hs (limited to 'tea-cleaner') 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 '_' - } -- cgit v1.2.3