Compare commits

..

No commits in common. "master" and "1.2" have entirely different histories.
master ... 1.2

8 changed files with 3982 additions and 5680 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [ncoevoet]

410
README.md
View File

@ -1,124 +1,43 @@
# ChanTracker — a Supybot plugin for ban tracking #
# ChanTracker : a supybot plugin for ban tracking #
This Supybot plugin keeps records of channel mode changes in an SQLite database and permits
management of them over time. It stores affected users, enabling deep searching through them,
reviewing actives, editing duration, showing logs, marking/annotating them, etc.
This supybot plugin keeps records of channel mode changes in a sqlite database and permits management of them over time. It stores affected users, enabling deep searching through them, reviewing actives, editing duration, showing logs, marking/annotating them, etc.
The plugin is used in various and large channels on Libera.Chat and other networks.
This version works with Python 3.
## Installation ##
Note that you may need a newer version of Limnoria than your distribution provides, so you may
need to install it from the source code or via PyPI/`pip` to make the plugin function.
(Currently it requires Limnoria version 2018.04.14 or newer.)
You can install the plugin with:
pip3 install git+https://github.com/ncoevoet/ChanTracker.git
Or with Limnoria versions older than 2020.05.08, in your bot's `plugins` directory:
git clone https://github.com/ncoevoet/ChanTracker.git
Then `@load ChanTracker`.
The plugin is used in various and large channels on freenode and others networks
## Commands ##
@b,q,e,i [<channel>] [--perm] <nick|hostmask>[,<nick|hostmask>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] <reason>
+<mode> targets for duration; <reason> is mandatory, <-1> or empty means forever,
add --perm if you want to add it to permanent bans of channel
@ub,uq,ue,ui [<channel>] <nick|hostmask|*> [<nick|hostmask|*>] -- sets -<mode> on them; if * is given, remove them all
@k,r [<channel>] <nick> [<reason>] -- kick or force-part <nick> with <reason> if provided
@edit <id>[,<id>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]
change expiry of an active mode change; <-1s> means forever, <0s> means remove
@mark <id>[,<id>] <message> -- add comment on a mode change
@editandmark <id>[,<id>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] [<reason>]
change expiry and mark of an active mode change; if you got this message while the bot
prompted you, your changes were not saved; <-1s> means forever, <0s> means remove
@info <id> -- summary of a mode change
@detail <id> -- logs of a mode change
@check [<channel>] <pattern> -- returns a list of users affected by a pattern
@affect <id> -- list users affected by a mode change
@match [<channel>] <nick|hostmask#username>
returns active modes that affect the given target; nick must be in a channel shared with the bot
@query [--deep] [--never] [--active] [--ids] [--channel=<channel>] <pattern|hostmask|comment>
search in tracking database; --deep to search in logs, --never returns items set forever and active,
--active returns only active modes, --ids returns only ids, --channel limits results to the specified channel
@pending [<channel>] [--mode=<e|b|q|l>] [--oper=<nick|hostmask>] [--never] [--ids] [--count] [--flood] [--duration [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]]
returns active items for --mode, filtered by --oper, --never (never expire), --ids (only ids),
--duration (item longer than), --count returns the total, --flood one message per mode
@modes [<channel>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] <mode> [<arg> ...]
sets the mode in <channel> to <mode>, sending the arguments given; <channel> is only
necessary if the message isn't sent in the channel itself, <delay> is optional
@ops [<reason>] -- triggers ops in the operators channel
@summary [<channel>] -- returns various statistics about channel activity
@weblink -- provides link to web interface
@addpattern [<channel>] <limit> <life> <mode>(bqeIkrd) [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] <pattern>
add a <pattern> which triggers <mode> for <duration> if the <pattern> appears
more often than <limit> (0 for immediate action) during <life> in seconds
@addregexpattern [<channel>] <limit> <life> <mode>(bqeIkrd) [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] /<pattern>/
add a <pattern> which triggers <mode> for <duration> if the <pattern> appears
more often than <limit> (0 for immediate action) during <life> in seconds
@rmpattern [<channel>] <id>[,<id>] -- remove patterns by <id>
@lspattern [<channel>] [<id|pattern>] -- return patterns in <channel> filtered by optional <id> or <pattern>
@addtmp [<channel>] <pattern> -- add temporary pattern, which follows repeat punishments
@rmtmp [<channel>] -- remove temporary patterns if any
@cflood [<channel>] [<permit>] [<life>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if a user sends more than <permit> (-1 to disable) messages during <life> (in seconds)
@crepeat [<channel>] [<permit>] [<life>] [<mode>] [<duration>] [<minimum>] [<probability>] [<count>] [<patternLength>] [<patternLife>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if <permit> (-1 to disable) repetitions are found during <life> (in seconds);
it will create a temporary lethal pattern with a mininum of <patternLength>
(-1 to disable pattern creation); <probablity> is a float between 0 and 1
@chl [<channel>] [<permit>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) during <duration> (in seconds)
if <permit> (-1 to disable) channel nicks are found in a message
@cnotice [<channel>] [<permit>] [<life>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if <permit> (-1 to disable) messages are channel notices during <life> (in seconds)
@ccycle [<channel>] [<permit>] [<life>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if <permit> (-1 to disable) parts/quits are received by a host during <life> (in seconds)
@cclone [<channel>] [<permit>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if <permit> (-1 to disable) users with the same host join the channel
@cnick [<channel>] [<permit>] [<life>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) during <duration> (in seconds)
if a user changes nick <permit> (-1 to disable) times during <life> (in seconds)
@ccap [<channel>] [<permit>] [<life>] [<mode>] [<duration>] [<probability>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if <permit> (-1 to disable) messages during <life> (in seconds)
contain more than <probability> (float between 0-1) uppercase chars
@cbad [<channel>] [<permit>] [<life>] [<mode>] [<duration>]
return channel's config or apply <mode> (bqeIkrdD) for <duration> (in seconds)
if a user triggers <permit> (-1 to disable) channel protections during <life> (in seconds)
@cautoexpire [<channel>] [<autoexpire>]
return channel's config or auto remove new elements after <autoexpire> (-1 to disable, in seconds)
!b,e,i,q [<channel>] <nick|hostmask>[,<nick|hostmask>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] [<-1> or empty means forever] <reason>) -- +mode targets for duration <reason> is mandatory
!ub,ue,ui,uq [<channel>] <nick|hostmask|*> [<nick|hostmask>]) -- sets -mode on them, if * found, remove them all
!check [<channel>] <pattern> returns list of users who will be affected by such pattern
!edit <id> [,<id>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] [<-1>] means forever) -- change expiration of some active modes
!info <id> returns information about a mode change
!affect <id> returns affected users by a mode placed
!mark <id> [,<id>] <message> add a comment about a mode change
!editandmark <id> [,<id>] [,<id>] [<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s] [<-1>] [<comment>] edit duration and add comment on a mode change
!pending [<channel>] (pending [--mode=<e|b|q|l>] [--oper=<nick|hostmask>] [--never] [<channel>] ) -- returns active items for --mode if given, filtered by --oper if given, --never never expire only if given
!query [--deep] [--never] [--active] [--channel=<channel>] <pattern|hostmask|comment>) -- search inside ban database, --deep to search on log, --never returns items set forever and active, --active returns only active modes, --channel reduces results to a specific channel
!match [<channel>] <nick|hostmask> returns list of modes that affects the nick,hostmask given
!detail <id> returns log from a mode change
!remove [<channel>] <nick> [<reason>] do a force part on <nick> in <channel> with <reason> if provided
!modes [<channel>] <mode> Sets the mode in <channel> to <mode>, sending the arguments given, bot will ask for op if needed.
!summary [<channel>] returns some stats about <channel>
## General Usage ##
The bot can be used to place and remove bans (rather than the op setting channel modes directly).
For example, to quiet the argumentative user 'ian' for 10 minutes and ban the spammer 'ham' for a month:
The bot can be used to place and remove bans (rather than the the op setting channel modes directly). For example, to quiet the argumentative user 'ian' for 10 minutes and ban the spammer 'ham' for a month:
@q ian 10m argumentative again
@b ham 30d silly spammer
@b foo 1h30m must stop
!q ian 10m argumentative again
!b ham 30d silly spammer
!b foo 1h30m must stop
These can also be done via a private message to the bot, although you must include the channel in the message:
/msg mybigbadbot q #myChannel ian 10m argumentative again
/msg mybigbadbot b #myChannel ham 30d silly spammer
For each of these bans, the nick is used to generate a `*!*@host` ban. The desired mask can be given directly
to the bot instead of the nick. Also note that, by default, the bot will also kick users that have a ban set
against them (details below).
For each of these bans, the nick is used to generate a \*!\*@host ban. The desired mask can be given directly to the bot instead of the nick. Also note that, by default, the bot will also kick users that have a +b set against them (details below).
Alternatively, the bot can be used to just track the mode changes, with ops using the capabilities of their
own IRC clients to set bans. The same sequence as before:
Alternatively, the bot can be used just to track the mode changes, with ops using the capabilities of their own IRC clients to set bans. The same sequence as before:
/msg chanserv #myChannel op
/mode #myChannel +q *!*@ranty.ian.home
@ -127,9 +46,7 @@ own IRC clients to set bans. The same sequence as before:
/kick ham
/msg mybigbadbot 30d silly spammer
If you annotate the bans within 5 minutes of setting them, then you can do so without any additional syntax as above.
Otherwise, the `pending`, `edit`, `mark`, and `editandmark` commands can be used to provide annotations and expiration
information. For example, if you had not immediately annotated the quiet:
If you annotate the bans within 3 minutes of setting them, then you can do so without any additional syntax as above; if you miss that window or are otherwise not setting bans via the bot, the `pending`, `edit`, `mark` and `editandmark` commands can be used to provide annotations and expiration information. For example, if you had not immediately annotated the quiet.
/msg mybigbadbot query ian!*@*
/msg mybigbadbot pending #myChannel
@ -138,253 +55,196 @@ information. For example, if you had not immediately annotated the quiet:
/msg bigbadbot mark 18 even more argumentative and EXTREMELY ANGRY
/msg bigbadbot editandmark 18 20m even more argumentative and EXTREMELY ANGRY
ChanTracker also allows you to work out which users would be affected by a ban before it is placed and which bans affect
a given user (assuming the bot shares a channel with the user).
ChanTracker also allows you to work out which users would be affected by a ban before it is placed and which bans affect a given user ( assuming the bot shares a channel with the user ).
/msg bigbadbot check #myChannel *!*@*.com <-- oops?
/msg bigbadbot match #myChannel ian <-- will return
/msg bigbadbot match #myChannel ian <-- will return
<bigbadbot> [#21 +b ian!*@* by me!~me@example.net expires at 2014-04-13 15:20:03 GMT] "even angrier"
## Settings ##
If you want the bot to manage its own op status, you must change this setting:
**If you want the bot to manage its own op status, you must change the config value**:
@config supybot.plugins.ChanTracker.doNothingAboutOwnOpStatus False
@config channel #myChannel supybot.plugins.ChanTracker.doNothingAboutOwnOpStatus True
!config supybot.plugins.ChanTracker.doNothingAboutOwnOpStatus False
!config channel #myChannel supybot.plugins.ChanTracker.doNothingAboutOwnOpStatus True
After `doNothingAboutOwnOpStatus` is changed to `False`, the bot will deop in each channel it is opped in, so take a look at:
After the 'doNothingAboutOwnOpStatus' changed to False, bot will deop in each channel is in (if opped) so take a look at:
@config supybot.plugins.ChanTracker.keepOp False
@config channel #myChannel supybot.plugins.ChanTracker.keepOp True
!config supybot.plugins.ChanTracker.keepOp False
!config channel #myChannel supybot.plugins.ChanTracker.keepOp True
You should decrease the ping interval, because when the bot requests a load of data when it joins a channel,
sometimes it could be throttled by the server, and the bot will retry at next ping/pong:
You should increase the ping interval because when the bot joins a channel because it requests lots of data and sometimes the server takes time to answer
@config supybot.protocols.irc.ping.interval 60
!config supybot.protocols.irc.ping.interval 3600
Here is the list of data requested by the bot at join:
Here list of data requested by the bot at join:
JOIN :#channel
MODE :#channel
MODE :#channel b
MODE :#channel q
WHO #CHANNEL %tuhnairf,1
WHO #CHANNEL %tnuhiar,42
...and if opped or at first op:
and if opped or at first op:
MODE :#channel e
MODE :#channel I
The channel modes that will be tracked are currently defined here (qb, and eI if opped -- only ops can see the e and I lists for a channel):
The channel modes that will be tracked are currently defined here (by default `bq`, and `eI` if opped
-- only ops can see the `e` and `I` lists for a channel):
!config supybot.plugins.ChanTracker.modesToAsk
!config supybot.plugins.ChanTracker.modesToAskWhenOpped
!config channel #myChannel supybot.plugins.ChanTracker.modesToAsk b, q
!config channel #myChannel supybot.plugins.ChanTracker.modesToAskWhenOpped e
@config supybot.plugins.ChanTracker.modesToAsk
@config supybot.plugins.ChanTracker.modesToAskWhenOpped
@config channel #myChannel supybot.plugins.ChanTracker.modesToAsk b, q
@config channel #myChannel supybot.plugins.ChanTracker.modesToAskWhenOpped e
The command used by the bot to op itself is editable here:
The command used by the bot to op itself is editable here, where `$channel` and `$nick` will be replaced
with the target channel and the bot's nick at runtime:
!config supybot.plugins.ChanTracker.opCommand by default it's "CS OP $channel $nick"
@config supybot.plugins.ChanTracker.opCommand "PRIVMSG ChanServ :OP $channel $nick"
Where $channel and $nick will be replaced by targeted channel and bot's nick at runtime, so you could replace it with :
You can also tell the bot to use ChanServ for quiet and unquiet, if it has the `+r` flag on Atheme services:
!config supybot.plugins.ChanTracker.opCommand "PRIVMSG ChanServ :OP $channel $nick"
@config supybot.plugins.ChanTracker.useChanServForQuiets True
@config supybot.plugins.ChanTracker.quietCommand "PRIVMSG ChanServ :QUIET $channel $hostmask"
@config supybot.plugins.ChanTracker.unquietCommand "PRIVMSG ChanServ :UNQUIET $channel $hostmask"
You can also tell the bot to use ChanServ for quiet and unquiet, if it has +r flag, on freenode:
!config supybot.plugins.ChanTracker.useChanServForQuiets True
!config supybot.plugins.ChanTracker.quietCommand "PRIVMSG ChanServ :QUIET $channel $hostmask"
!config supybot.plugins.ChanTracker.quietCommand "PRIVMSG ChanServ :UNQUIET $channel $hostmask"
For more readable date information in output, you should change this:
@config supybot.reply.format.time.elapsed.short True
!config supybot.reply.format.time.elapsed.short True
The bot can have a *reporting channel* like an -ops channel, where it forwards a lot of important
information about channel activity. You can set it globally or per channel:
The bot can have a "reporting channel" like an -ops channel, where it forwards a lot of important information about channel activity. You can set it globally or per channel:
@config supybot.plugins.ChanTracker.logChannel #myGeneralSecretChannel
@config channel #myChannel supybot.plugins.ChanTracker.logChannel #myChannel-ops
!config supybot.plugins.ChanTracker.logChannel #myGeneralSecretChannel
!config channel #myChannel supybot.plugins.ChanTracker.logChannel #myChannel-ops
You can use colors in it:
You can tweak which information you would like to be forwarded to the reporting channel. Some reporting is activated by default like topic changes, mode changes, etc, some not, like bot's ban/quiet edit/mark etc. Take a look at:
@config channel #myChannel supybot.plugins.ChanTracker.useColorForAnnounces True
!search supybot.plugins.ChanTracker.announce
!config help supybot.plugins.ChanTracker.announceModes
You can tweak which information you would like to be forwarded to the reporting channel.
Some reporting is activated by default, like topic changes, mode changes, etc.
While some are not, like ban/quiet and edit/mark by the bot, etc. Take a look at:
If desired, the bot can send a private message to the op that sets a tracked mode. Note the op must be known as channel's op by the bot; the bot owner automatically has that capability:
@search supybot.plugins.ChanTracker.announce
@config help supybot.plugins.ChanTracker.announceModes
If desired, the bot can send a private message to the op that sets a tracked mode. Note the op must
be known as channel op by the bot; the bot owner automatically has that capability:
@config channel #myChannel supybot.plugins.ChanTracker.askOpAboutMode True
!config channel #myChannel supybot.plugins.ChanTracker.askOpAboutMode True
You can add op capability to someone by doing:
!user register opaccount password
!hostmask add opaccount *!*@something
!admin capability add opaccount #myChannel,op
@user register opaccount password
@hostmask add opaccount *!*@something
@admin capability add opaccount #myChannel,op
The bot can set a default duration for new tracked mode changes, in order to auto remove them:
The bot can set a default duration for new tracked mode changes, in order to automatically remove them:
!config channel #myChannel supybot.plugins.ChanTracker.autoExpire 3600 (1h)
@config channel #myChannel supybot.plugins.ChanTracker.autoExpire 3600 (1 hour)
The plugin can create persistent bans to help manage large ban lists that exceed the IRCd's limits on the length of ban lists. The plugin can remove bans from the IRCd ban list while checking all joining users against its own lists. If a user matches, then the IRCd ban is reinstated:
The plugin can create persistent bans to help manage large ban lists that exceed the IRCd's limits on
the length of ban lists. It can remove bans from the IRCd ban list while checking all joining users
against its own lists. If a user matches, then the IRCd ban is reinstated:
!config channel #myChannel supybot.plugins.ChanTracker.useChannelBansForPermanentBan true
!channel ban add #myChannel *!*@mask
!b #example baduser,baduser2 --perm 1w stop trolling ( --perm add computed hostmasks to Channel.ban )
@config channel #myChannel supybot.plugins.ChanTracker.useChannelBansForPermanentBan true
@channel ban add #myChannel *!*@mask
@b #example --perm baduser,baduser2 1w stop trolling (--perm adds computed hostmasks to Channel.ban)
With autoExpire enabled, the IRCd list is pruned as appropriate and bans are rotated in a way to not reveal the pattern used for the match. Due to a supybot limitation, extended bans are not supported with this feature.
With `autoExpire` enabled, the IRCd list is pruned as appropriate and bans are rotated in a way to not reveal
the pattern used for the match. Due to a Supybot limitation, extended bans are not supported with this feature.
If supported by the ircd, the bot can track account changes and get GECOS and username information when a user joins the channel. This requires ircd CAP features: http://tools.ietf.org/html/draft-mitchell-irc-capabilities-01
If supported by the IRCd, the bot can track account changes and get GECOS and username information when a user
joins the channel. This requires IRCd CAP features: https://ircv3.net/specs/extensions/capability-negotiation.html
The plugin also supports extended bans/quiets including $r, $x, $a (real name, full match and account name, respectively). If you want the plugin to support your IRCd extended bans, please, report a bug or contact me directly.
The plugin also supports extended bans/quiets including `$a`, `$r`, `$x`, and `$j` (account name, real name, full match,
and ban channel). If you want the plugin to support your IRCd's extended bans, please file a feature request or contact
us via our IRC channel.
By default, if the bot is asked to set a ban (+b), it will also kick affected users (Note: bot will only kick people if the ban was set by the bot -- if an op places the ban, the bot will not kick affected users). See:
By default, if the bot is asked to set a ban (`+b`), it will also kick affected users. See:
!config supybot.plugins.ChanTracker.kickMode
!config supybot.plugins.ChanTracker.kickMessage
@config supybot.plugins.ChanTracker.kickMode
@config supybot.plugins.ChanTracker.kickMessage
@config help supybot.plugins.ChanTracker.kickOnMode
The bot will remove exemption modes (that is exempt `e`, or invite exempt `I`) for people banned if
`doActionAgainstAffected` for the given channel is `True`.
The bot will remove exception modes (that is exempt e, or invite exempt I) for people banned if 'doActionAgainstAffected' for given channel is True.
## Channel Protection ##
The plugin has a lot of built-in channel protection features that can be enabled either
individually and per-channel, or globally:
The plugin has a lot of built-in channel protection features that can be enabled either individually and per channel, or globally:
- flood detection
- low-rate flood detection: flooding but with client rate-limiting
- repeat detection
- massRepeat detection: when same message comes from different users
- capslock: detect people who are EXTREMELY ANGRY
- ctcp: detect sending CTCPs to the channel
- ctcp: detect sending CTCP to the channel
- notices: detect sending notices to the channel
- hilight: nick spam
- nick: nick change spam
- cycle: join/part flood
- massJoin
- evades of quiet/bans via gateway (if `resolveIp` is enabled)
- clone detection
You should tweak settings to fit your needs, do not use default values. It really depends
on the channel's population and usage...
You should tweak settings to fits your needs, do not use default values. It really depends channel's population and usage ...
Each of those detections has the same kind of settings:
Each of those detections has the same kind of settings: there is *Permit (-1 to disable), *Life (which is the time interval over which the bot will track previous messages/behaviour), *Mode (which allows you to select which action you want to use against the user). The action modes that can be set are:
- `*Permit`: (-1 to disable)
- `*Life`: time interval over which the bot will track previous messages/behaviour
- `*Mode`: allows you to select which action you want to use against the user
- `*Duration`: duration of the action taken
- `*Comment`: (empty for no comment)
- q : quiet the user
- b : ban the user
- k : kick the user
- r : remove (force part) the user, if the IRCd has the feature. 'REMOVE $channel $nick :$reason'
- d : debug > forward action to logChannel if configured
The action modes that can be set are:
For bans (b and q mode), you can choose the *Duration of the quiet/ban, and add a *Mark on the related quiet/ban. The 'bad' settings, when enabled (badPermit > -1) keeps track of users who did something wrong during badLife, and can end to badMode if the user exceeds the limit.
- `q`: quiet the user
- `b`: ban the user
- `k`: kick the user
- `r`: remove (force-part) the user, if the IRCd has the feature
- `d`: debug -- forward action to log channel, if configured
Example: flood control: to quiet for 1 minute anyone who sends more than 4 messages in 7 seconds to #channel; if the user continues to flood, after 2 times he will be banned:
For bans (`b` and `q` modes), you can choose the `*Duration` of the quiet/ban, and set a `*Comment` for it.
The `bad` settings, when enabled (`badPermit > -1`) keep track of users who did something wrong during `badLife`,
and can lead to `badMode` if the user exceeds the limit.
!config channel #channel supybot.plugins.ChanTracker.floodPermit 4 <-- max number of messages allowed
!config channel #channel supybot.plugins.ChanTracker.floodLife 7 <-- in 7 seconds
!config channel #channel supybot.plugins.ChanTracker.floodMode q <-- quiet the user
!config channel #channel supybot.plugins.ChanTracker.floodDuration 60 <-- for 60 seconds
!config channel #channel supybot.plugins.ChanTracker.badPermit 2 <-- if user does that 3 times,
!config channel #channel supybot.plugins.ChanTracker.badMode b <-- ban them
**Example:** Flood control -- to quiet for 1 minute anyone who sends more than 4 messages in 7 seconds to #channel;
if the user continues to flood, after 3 times within 5 minutes they will be banned:
Additionally, the can track how many bad actions occur over a period of time and if a threshold is passed, this constitutes an attack on the channel. The attack* settings, when enabled keeps track of bad actions, and if the number exceeds attackPermit within attackLife, some specific channel modes are set for an attackDuration.
@config channel #channel supybot.plugins.ChanTracker.floodPermit 4 <-- max number of messages allowed
@config channel #channel supybot.plugins.ChanTracker.floodLife 7 <-- in 7 seconds
@config channel #channel supybot.plugins.ChanTracker.floodMode q <-- quiet the user
@config channel #channel supybot.plugins.ChanTracker.floodDuration 60 <-- for 60 seconds
@config channel #channel supybot.plugins.ChanTracker.badPermit 2 <-- if user does that 3 times (more than 2)
@config channel #channel supybot.plugins.ChanTracker.badLife 300 <-- during 5 minutes
@config channel #channel supybot.plugins.ChanTracker.badMode b <-- ban them
Example: not flooding: catch a wave of bots which sends the same message from different hosts:
Additionally, the bot can track how many bad actions occur over a period of time and if a threshold is passed,
this constitutes an attack on the channel. The `attack*` settings, when enabled keep track of bad actions,
and if the number exceeds `attackPermit` within `attackLife`, some specified channel modes are set for `attackDuration`.
!config channel #channel supybot.plugins.ChanTracker.massRepeatChars 200 <-- enable check only if there is at least 200 chars
!config channel #channel supybot.plugins.ChanTracker.massRepeatPermit 0 <-- that means if first message matchs the seconds, it will trigger it
!config channel #channel supybot.plugins.ChanTracker.massRepeatLife 60 <-- don't keep messages too long in memory, to avoid false positive
!config channel #channel supybot.plugins.ChanTracker.massRepeatPercent 0.85 <-- set a low value for similarity, in order to catch them if there is some random chars in the messages
!config channel #channel supybot.plugins.ChanTracker.massRepeatMode b
!config channel #channel supybot.plugins.ChanTracker.massRepeatDuration 1800 <- ban for 30 minutes
!config channel #channel supybot.plugins.ChanTracker.attackPermit 2 <- if bot triggers 3 actions during
!config channel #channel supybot.plugins.ChanTracker.attackLife 300 <- 5 minutes
!config channel #channel supybot.plugins.chantracker.attackMode +rq $~a <- then bot will set those modes
!config channel #channel supybot.plugins.chantracker.attackDuration 1800 <- for 30 minutes
!config channel #channel supybot.plugins.chantracker.attackUnMode -rq $~a <- and bot will set those modes after 30 minutes
Example: a user repeating the same thing: (use repeat detection rather than massRepeat for this):
**Example:** Bot flooding -- catch a wave of bots which send the same message from different hosts:
!config channel #channel supybot.plugins.ChanTracker.repeatPermit 3 <-- triggered after 3 similar message
!config channel #channel supybot.plugins.ChanTracker.repeatLife 40 <-- keep previous messages during 40 seconds
!config channel #channel supybot.plugins.ChanTracker.repeatPercent 0.88 <-- 1.00 for identical message, don't go too lower, you will get false positive
!config channel #channel supybot.plugins.ChanTracker.repeatMode q <-- quiet
!config channel #channel supybot.plugins.ChanTracker.repeatDuration 180 <-- for 3 minutes
@config channel #channel supybot.plugins.ChanTracker.attackPermit 2 <-- if bot triggers 3 bad actions (more than 2)
@config channel #channel supybot.plugins.ChanTracker.attackLife 300 <-- during 5 minutes
@config channel #channel supybot.plugins.chantracker.attackMode +rq $~a <-- then bot will set these modes
@config channel #channel supybot.plugins.chantracker.attackDuration 1800 <-- for 30 minutes
@config channel #channel supybot.plugins.chantracker.attackUnMode -rq $~a <- and bot will set these modes after they passed
Even with all these channel protection features, the bot will do nothing against users with protected capabilities (#channel,protected).
**Example:** A user repeating the same thing:
## Other tips ##
@config channel #channel supybot.plugins.ChanTracker.repeatPermit 3 <-- triggered after 3 similar messages
@config channel #channel supybot.plugins.ChanTracker.repeatLife 40 <-- keep previous messages during 40 seconds
Maintaining separate bots for the banning/bantracking functions and other factoid, snarfing or amusement functions is good practice.
@config channel #channel supybot.plugins.ChanTracker.repeatMinimum 8 <-- minimum size of candidate patterns
@config channel #channel supybot.plugins.ChanTracker.repeatPercent 0.88 <-- 1.00 for identical message, don't go too low or you will get false positives
@config channel #channel supybot.plugins.ChanTracker.repeatCount 6 <-- or the number of times a pattern is repeated in a single message
@config channel #channel supybot.plugins.ChanTracker.repeatPatternMinimum 12 <-- mininum size of temporary lethal pattern
@config channel #channel supybot.plugins.ChanTracker.repeatPatternLife 120 <-- life duration of those patterns in seconds
If the main purpose of your bot is to manage bans etc, and never interacts with users you should, as owner remove all plugin with 'owner defaultcapabilities remove <pluginname>', it will prevent the bot to answer to various command, and being used as a flood tool by others (like !echo SPAM). You could otherwise change the value of `config help supybot.capabilities.default` but be prepared to waste a lot of time each time you add a new user account on your bot.
@config channel #channel supybot.plugins.ChanTracker.repeatMode q <-- quiet
@config channel #channel supybot.plugins.ChanTracker.repeatDuration 3600 <-- for 1 hour
If 'supybot.capabilities.default' is changed to False, then, when you want to grant access to command for someone, you must do it that way:
## `protected` Capability ##
!admin capability add accountname User
!admin capability add accountname User.whoami
!admin capability add accountname whoami
You must remove the `protected` capability given by default to everyone, because the bot will not do anything
against users having this capability:
If your bot manage differents channels or community, remove all User.action from defaultcapabilities, create one user per channel/community, and add ops's hostmasks into it, it's easier to manage that way. Until you have someone with rights in 2 community/channels who will need a separate account.
@defaultcapability remove protected
You should keep your bot quiet as possible, it should not replies to error, user without capabilities, etc :
## Other Tips ##
Maintaining separate bots for the banning/bantracking functions and other factoid, snarfing,
or amusement functions is good practice.
If the main purpose of your bot is to manage bans etc and never interact with users, you should remove all plugins
from the default capabilities, which will prevent the bot from responding to various commands and being used as a
flood tool by others (like with `@echo SPAM`):
@defaultcapability remove <pluginname>
You could otherwise change the value of `supybot.capabilities.default`, but be prepared to waste a lot of time
each time you add a new user account on your bot. If the setting is changed to `False`, then when you want to grant
access to a command for someone, you must do it this way:
@admin capability add <accountname> User
@admin capability add <accountname> User.whoami
@admin capability add <accountname> whoami
If your bot manages different channels or communities, remove all `User.<action>` from the default capabilities,
create one user per channel/community, and add ops' hostmasks to it -- it's easier to manage this way.
Until you have someone with rights in multiple channels/communities, who will need a separate account.
You should keep your bot as quiet as possible. It should not reply to errors, users without capabilities, etc:
@config supybot.reply.error.noCapability True
@config supybot.replies.genericNoCapability ""
@config supybot.abuse.flood.command.invalid.notify False
@config supybot.reply.whenNotCommand False
@config supybot.reply.error.detailed False
@config supybot.replies.error ""
@config defaultcapability remove channel.nicks
@config defaultcapability remove channel.alert
@config defaultcapability remove alias.add
@config defaultcapability remove config
@config defaultcapability remove help
@config defaultcapability remove list
There are other commands that are prone to abuse as well. It's better to use this command:
@config supybot.capabilities.default False
supybot.reply.error.noCapability: True
supybot.reply.whenNotCommand: False
supybot.reply.error.detailed: False
You should also disable help, config, list until needed for registered users on the bot.
It works with any version of supybot, vanilla, limnoria, etc
## Bugs and Features ##
Requests can be made via https://github.com/ncoevoet/ChanTracker
or in #chantracker on chat.libera.chat
Requests can be made via https://github.com/ncoevoet/ChanTracker or in private message to niko on chat.freenode.net.

View File

@ -25,6 +25,7 @@
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
@ -36,7 +37,7 @@ import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = "1.5"
__version__ = "1.2"
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.authors.unknown
@ -50,12 +51,8 @@ __url__ = 'https://github.com/ncoevoet/ChanTracker'
from . import config
from . import plugin
from . import server
from importlib import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
reload(server)
from imp import reload
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!

407
config.py
View File

@ -25,12 +25,12 @@
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
@ -42,427 +42,320 @@ def configure(advanced):
ChanTracker = conf.registerPlugin('ChanTracker')
## global settings
conf.registerGlobalValue(ChanTracker, 'pool',
registry.Integer(-1, """delay between two checks about mode removal, in seconds.
Note, check is also based on irc activity, so removal may be delayed a bit, -1 to disable delay"""))
registry.Integer(-1, """delay between two checks about mode removal, in seconds. Note, check is also based on irc activity, so removal may be delayed a bit, -1 to disable delay"""))
conf.registerGlobalValue(ChanTracker, 'CAPS',
registry.CommaSeparatedListOfStrings(['account-notify','extended-join'], """CAP asked to the IRCd, that way bot can track username and account changes, without any additional request"""))
conf.registerGlobalValue(ChanTracker, 'logsSize',
registry.PositiveInteger(1, """number of messages to keep in logs. Note, this is per nick - not per nick per channel"""))
registry.PositiveInteger(60, """number of messages to keep in logs. Note, this is per nick - not per nick per channel"""))
conf.registerGlobalValue(ChanTracker, 'quietCommand',
registry.String("PRIVMSG ChanServ :QUIET $channel $hostmask",
"""command issued to quiet a user; $channel and $hostmask will be replaced at runtime"""))
registry.String("CS QUIET $channel $hostmask","""command issued to quiet a user; $channel and $hostmask will be replaced at runtime"""))
conf.registerGlobalValue(ChanTracker, 'unquietCommand',
registry.String("PRIVMSG ChanServ :UNQUIET $channel $hostmask",
"""command issued to unquiet a user; $channel and $hostmask will be replaced at runtime"""))
registry.String("CS UNQUIET $channel $hostmask","""command issued to unquiet a user $channel and $hostmask will be replaced at runtime"""))
conf.registerGlobalValue(ChanTracker, 'announceNagInterval',
registry.Integer(-1, """interval between two checks about announceNagMode, this setting is global"""))
conf.registerGlobalValue(ChanTracker, 'resolveIp',
registry.Boolean(True, """trying to resolve host's ip with socket, could add latency"""))
## per-channel settings
conf.registerChannelValue(ChanTracker, 'enabled',
registry.Boolean(False, """monitor the channel and act following the rest of the settings"""))
conf.registerChannelValue(ChanTracker, 'modeD',
registry.String("", """special mode if you want to use some specific stuff;
$hostmask (*!*@*), $klinemask (*@*), $host, $channel, $reason, $nick and $duration are available"""))
conf.registerChannelValue(ChanTracker, 'useAccountBanIfPossible',
registry.Boolean(False, """bot will use account bans when user is identified, only works for b,q,e,I and m (chantracker.m)"""))
conf.registerChannelValue(ChanTracker, 'avoidOverlap',
registry.Boolean(False, """avoid overlap between items, bot will try to use existing items against users, some limitations with extended bans"""))
registry.Integer(300,"""interval between two check about announceNagMode, this setting is global."""))
conf.registerChannelValue(ChanTracker, 'useIpForGateway',
registry.Boolean(False, """use *!*@*ip bans instead of *!ident@gateway/* when gateway cloak is found and ends with ip.*"""))
registry.Boolean(False, """use *!*@*ip bans instead of *!ident@gateway/* when gateways cloak is found and ends with ip.*"""))
conf.registerChannelValue(ChanTracker, 'triggerOps',
registry.Boolean(False, """!ops triggers a message in logChannel"""))
conf.registerChannelValue(ChanTracker, 'allowOpToConfig',
registry.Boolean(False, """grant channel operators the ability to configure some ChanTracker protections for their channel"""))
conf.registerChannelValue(ChanTracker, 'ignoreVoicedUser',
registry.Boolean(False, """exempt voiced users from channel protections"""))
#now per channel
conf.registerChannelValue(ChanTracker, 'opCommand',
registry.String("PRIVMSG ChanServ :OP $channel $nick", """command used to obtain channel operator status"""), opSettable=False)
registry.String("CS OP $channel $nick", """command used to obtain channel operator mode"""))
conf.registerChannelValue(ChanTracker, 'modesToAsk',
registry.CommaSeparatedListOfStrings(['b', 'q'], """list of channel modes to sync into the bot's tracking database when it joins the channel"""))
registry.CommaSeparatedListOfStrings(['b','q'], """list of channel modes to sync into the bot's tracking database when it joins the channel"""))
conf.registerChannelValue(ChanTracker, 'modesToAskWhenOpped',
registry.CommaSeparatedListOfStrings(['e', 'I'], """list of channel modes to sync into the bot's tracking database when it is opped"""))
registry.CommaSeparatedListOfStrings(['e','I'], """list of channel modes to sync into the bot's tracking database when it is opped"""))
conf.registerChannelValue(ChanTracker, 'ignoreOnAbuse',
registry.Boolean(False, """adds abusers on ignore for the punishment period"""))
## related to ban tracking
# per channel settings
# related to ban tracking
conf.registerChannelValue(ChanTracker, 'autoExpire',
registry.Integer(-1, """default expiration time for newly placed bans; -1 disables auto-expiration, otherwise it's in seconds"""))
conf.registerChannelValue(ChanTracker, 'autoRemoveUnregisteredQuiets',
registry.Boolean(True, """auto remove from database unregistered quiets once expired -q $~a"""))
conf.registerChannelValue(ChanTracker, 'allowPublicInfo',
registry.Boolean(False, """allow !info to be returned in where it was called if True"""))
# related to logChannel
conf.registerChannelValue(ChanTracker, 'removeAllBans',
registry.Boolean(False, """prevent accidental removal of all bans"""))
conf.registerChannelValue(ChanTracker, 'removeAllQuiets',
registry.Boolean(False, """prevent accidental removal of all quiets"""))
conf.registerChannelValue(ChanTracker, 'removeAllExempts',
registry.Boolean(False, """prevent accidental removal of all exempts"""))
conf.registerChannelValue(ChanTracker, 'removeAllInvites',
registry.Boolean(False, """prevent accidental removal of all invites"""))
## related to logChannel
conf.registerChannelValue(ChanTracker, 'logChannel',
registry.String("", """where bot announces op actions; it is highly recommended to set an appropriate operators channel
to receive the various useful messages, nick can be used"""), opSettable=False)
conf.registerChannelValue(ChanTracker, 'useSmartLog',
registry.Boolean(True, """compress multiple announce messages at the same time into a single one"""))
registry.String("", """where bot announces op's actions; it is highly recommended to set an appropriate operator's channel to receive the various useful messages"""))
conf.registerChannelValue(ChanTracker, 'useColorForAnnounces',
registry.Boolean(False, """use colors for announce messages"""))
conf.registerChannelValue(ChanTracker, 'announceOthers',
registry.Boolean(True, """forward messages from quieted/banned users to logChannel; used when bot stays opped and channel is +z (reduced moderation).
messages from users flagged as bad, or when channel is under attack will not be forwarded"""))
registry.Boolean(True,"""forward messages from quieted/banned users to logChannel; used when bot stays opped and channel is +z (reduced moderation).
Messages from users flagged as bad, or when channel is under attack will not be forwarded"""))
conf.registerChannelValue(ChanTracker, 'announceModeMadeByIgnored',
registry.Boolean(True, """announce channel modes made by ignored user"""))
registry.Boolean(True,"""announce channel modes made by ignored user"""))
conf.registerChannelValue(ChanTracker, 'announceWithNotice',
registry.Boolean(False, """use NOTICE instead of PRIVMSG to logChannel"""))
registry.Boolean(False,"""use NOTICE instead of PRIVMSG to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceModes',
registry.CommaSeparatedListOfStrings(['b', 'q', 'e', 'I', 'r', 'l', 'k', 'n', 't', 'F', 'i', 't', 's', 'n', 'c', 'C'],
"""announce modes listed to logChannel"""))
registry.CommaSeparatedListOfStrings(['b','q','e','I','r','l','v','o','h','k','n','t','F','i','t','s','n','c','C'],"""announce modes listed to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceModeSync',
registry.Boolean(False, """announce to logChannel that synchronisation of channel modes to tracking database has completed"""))
registry.Boolean(False,"""announce to logChannel that synchronisation of channel modes to tracking database has completed"""))
conf.registerChannelValue(ChanTracker, 'announceKick',
registry.Boolean(True, """announce kick, remove, kill and kline to logChannel"""))
registry.Boolean(True,"""announce kick, remove, kill and kline to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceTopic',
registry.Boolean(True, """announce topic changes to logChannel"""))
registry.Boolean(True,"""announce topic changes to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceEdit',
registry.Boolean(True, """announce new expiries on items to logChannel"""))
registry.Boolean(True,"""announce tracker item description edits to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceMark',
registry.Boolean(True, """announce new comments on items to logChannel"""))
registry.Boolean(True,"""announce item expiration settings (marks) to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceInTimeEditAndMark',
registry.Boolean(False, """announce new expiries and comments to logChannel when they are created by the do, q, b, e and i commands"""))
registry.Boolean(False,"""announce new comments (edits) and expiries (marks) to logChannel when they are created by the do, q, b, e, i commands"""))
conf.registerChannelValue(ChanTracker, 'announceMassRemoval',
registry.Boolean(False, """announce mass ban removals 'undo *', 'uq *', 'ub *' to logChannel"""))
registry.Boolean(False,"""announce mass ban removals 'undo *', 'uq *', 'ub *' to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceBotEdit',
registry.Boolean(False, """when banning based on a channel protection trigger (such as flood prevention), announce the item expiry to logChannel"""))
registry.Boolean(False,"""when banning based on a channel protection trigger (such as flood prevention), announce the items comment (edit) to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceBotMark',
registry.Boolean(False, """when banning based on a channel protection trigger (such as flood prevention), announce the item comment to logChannel"""))
registry.Boolean(False,"""when banning based on a channel protection trigger (such as flood prevention), announce the items expiry (mark) to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceNotice',
registry.Boolean(True, """announce channel notices to logChannel"""))
registry.Boolean(True,"""announce channel notices to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceCtcp',
registry.Boolean(True, """announce channel ctcps to logChannel"""))
registry.Boolean(True,"""announce channel ctcps to logChannel"""))
conf.registerChannelValue(ChanTracker, 'announceNagMode',
registry.CommaSeparatedListOfStrings([], """bot will announce that channel has such mode at announceNagInterval"""))
conf.registerChannelValue(ChanTracker, 'announceRepeatPattern',
registry.Boolean(True, """announce repeat pattern created to logChannel"""))
## other settings
# others settings
conf.registerChannelValue(ChanTracker, 'doNothingAboutOwnOpStatus',
registry.Boolean(True, """bot will never try to change its own op status"""))
registry.Boolean(True, """bot will never try to change his own op status"""))
conf.registerChannelValue(ChanTracker, 'keepOp',
registry.Boolean(False, """bot stays opped"""))
conf.registerChannelValue(ChanTracker, 'kickMode',
registry.CommaSeparatedListOfStrings(['b'], """bot will kick affected users when mode is triggered;
use with caution, if an op bans *!*@*, bot will kick everyone on the channel - see kickMax"""))
conf.registerChannelValue(ChanTracker, 'kickOnMode',
registry.Boolean(False, """if the bot is not the kickMode issuer, it will still kick; use with caution"""))
registry.CommaSeparatedListOfStrings(['b'], """bot will kick affected users when mode is triggered,
use if with caution, if an op bans *!*@*, bot will kick everyone on the channel"""))
conf.registerChannelValue(ChanTracker, 'kickMax',
registry.Integer(-1, """if > 0, disable kick if affected users > kickMax, avoid to clean up entire channel with ban like *!*@*"""))
registry.Integer(-1,"""if > 0, disable kick if affected users > kickMax, avoid to cleanup entire channel with ban like *!*@*"""))
conf.registerChannelValue(ChanTracker, 'kickMessage',
registry.CommaSeparatedListOfStrings(["You are banned from this channel"], """bot kick reason"""))
conf.registerChannelValue(ChanTracker, 'discloseOperator',
registry.Boolean(True, """reveals operator's nick on sockpuppet action via the bot !b, !q, !r, !k (via the kick reason or banMessage/quietMessage)"""))
conf.registerChannelValue(ChanTracker, 'banMessage',
registry.String("You have been banned on $channel", """set empty if you don't want the bot to tell something to the user
when they have been banned (by/via the bot); in any case, if channel is under attack, bot will not send message;
if filled, the operator nick will be given for accountability if differing"""))
conf.registerChannelValue(ChanTracker, 'banNotice',
registry.Boolean(True, """if False, private message is used, if banMessage is not empty"""))
registry.String("You are banned from this channel", """bot kick reason"""))
conf.registerChannelValue(ChanTracker, 'quietMessage',
registry.String("You have been quieted on $channel", """set empty if you don't want the bot to tell something to the user
when they have been quieted (by/via the bot); in any case, if channel is under attack, bot will not send message;
if filled, the operator nick will be given for accountability if differing"""))
registry.String("", """leave empty if you don't want the bot to tell something to the user when he has been quieted ( by/via the bot ), in any case, if channel is under attack: bot will not send message"""))
conf.registerChannelValue(ChanTracker, 'quietNotice',
registry.Boolean(True, """if False, private message is used, if quietMessage is not empty"""))
conf.registerChannelValue(ChanTracker, 'proxyMsgOnly',
registry.Boolean(True, """only send message to user on operator action if it was proxied through the bot"""))
registry.Boolean(False, """if False, private message is used, if 'quietMessage' is not empty"""))
conf.registerChannelValue(ChanTracker, 'trackAffected',
registry.Boolean(True, """bot tracks affected users by mode change; if you encounter too much lag or cpu usage, you could disable this feature,
but bot will never kick again affected users or remove voice/op/exempt etc of affected users"""))
registry.Boolean(True, """bot tracks affected users by mode change, if you encounter too much lags/cpu usage, you could disable this feature, but bot will never kick again affected users or remove voice/op/exempt etc of affected users"""))
conf.registerChannelValue(ChanTracker, 'doActionAgainstAffected',
registry.Boolean(True, """devoice, deop, dehalfop user affected by a mode change"""))
conf.registerChannelValue(ChanTracker, 'useChannelBansForPermanentBan',
registry.Boolean(True, """when users join the channel, check if user matches a permanent ban set in Channel plugin"""))
registry.Boolean(True, """when users join the channel, check if user matchs a permanent ban set in Channel plugin"""))
conf.registerChannelValue(ChanTracker, 'addKickMessageInComment',
registry.Boolean(False, """add kick message to mode comment in tracking database"""))
conf.registerChannelValue(ChanTracker, 'askOpAboutMode',
registry.Boolean(False, """in a private message, ask the op who added a mode about the duration of the ban and a comment on why it was set"""))
registry.Boolean(False,"""In a private message, ask the op who added a mode about the duration of the ban and a comment on why it was set"""))
conf.registerChannelValue(ChanTracker, 'checkEvade',
registry.Boolean(True, """bot will apply same duration and mode as the evaded ban; currently only works when someone identifies to an account
and the account is banned, or has ip computed"""))
registry.Boolean(True,"""bot will apply same duration and mode as the evaded ban, currently only works when someone identifies to an account, and the account is banned $a:account, and has ip computed"""))
conf.registerChannelValue(ChanTracker, 'useChanServForQuiets',
registry.Boolean(False, """if bot is not opped, use network services for quiet/unquiets"""))
registry.Boolean(False,"""if bot is not opped, use services for quiet / unquiets"""))
# related to channel's protection
## related to channel protection
# clone detection
conf.registerChannelValue(ChanTracker, 'clonePermit',
registry.Integer(-1, """number of clones allowed , -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'cloneMode',
registry.String('d', """mode used by the bot when clone detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'cloneDuration',
registry.PositiveInteger(60, """punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'cloneComment',
registry.String('clone detected', """comment added to tracking database, empty for no comment"""))
# flood detection
# flood detection settings
conf.registerChannelValue(ChanTracker, 'floodPermit',
registry.Integer(-1, """number of messages allowed during floodLife, -1 to disable"""))
registry.Integer(-1,"""Number of messages allowed during floodLife, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'floodLife',
registry.PositiveInteger(7, """duration in seconds before messages are removed from counter"""))
registry.PositiveInteger(7,"""Duration of messages's life in flood counter, in seconds"""))
conf.registerChannelValue(ChanTracker, 'floodMode',
registry.String('q', """mode used by the bot when flood detection is triggered"""))
registry.String('q',"""mode used by the bot when flood detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'floodDuration',
registry.PositiveInteger(60, """punishment duration in seconds"""))
registry.PositiveInteger(60,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'floodComment',
registry.String('flood detected', """comment added to tracking database, empty for no comment"""))
# another flood queue, for user with throttled irc client, who paste long text
registry.String('flood detected',"""comment added on mode changes database, empty for no comment"""))
# another flood queue, for user with throttled irc client, who copy / paste long text
conf.registerChannelValue(ChanTracker, 'lowFloodPermit',
registry.Integer(-1, """number of messages allowed during lowFloodLife, -1 to disable"""))
registry.Integer(-1,"""Number of messages allowed during lowFloodLife, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'lowFloodLife',
registry.PositiveInteger(13, """duration in seconds before messages are removed from counter"""))
registry.Integer(13,"""Duration of messages's life in lowFlood counter, in seconds"""))
conf.registerChannelValue(ChanTracker, 'lowFloodMode',
registry.String('q', """mode used by the bot when low flood detection is triggered"""))
registry.String('q',"""mode used by the bot when low flood detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'lowFloodDuration',
registry.PositiveInteger(180, """punishment duration in seconds"""))
registry.PositiveInteger(180,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'lowFloodComment',
registry.String('low flood detected', """comment added to tracking database, empty for no comment"""))
registry.String('low flood detected',"""comment added on mode changes database, empty for no comment"""))
# repeat detection
conf.registerChannelValue(ChanTracker, 'repeatPermit',
registry.Integer(-1, """number of triggers allowed during repeatLife, -1 to disable"""))
registry.Integer(-1,"""Number of repeated text allowed, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'repeatLife',
registry.PositiveInteger(12, """duration in seconds before triggers are removed from counter"""))
conf.registerChannelValue(ChanTracker, 'repeatMode',
registry.String('q', """mode used by the bot when repeat detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'repeatDuration',
registry.PositiveInteger(180, """punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'repeatComment',
registry.String('repeat detected', """comment added to tracking database, empty for no comment"""))
# pattern repeat detection
conf.registerChannelValue(ChanTracker, 'repeatMinimum',
registry.PositiveInteger(8, """minimum length of pattern candidates to detect repetitions"""))
registry.PositiveInteger(12,"""Duration of messages's life in repeatPermit counter in seconds"""))
conf.registerChannelValue(ChanTracker, 'repeatPercent',
registry.Probability(0.85, """percent of similarity required between pattern candidates"""))
conf.registerChannelValue(ChanTracker, 'repeatCount',
registry.PositiveInteger(5, """number of occurences of pattern candidates"""))
conf.registerChannelValue(ChanTracker, 'repeatPatternMinimum',
registry.Integer(-1, """minimum length to create automated pattern; if found, triggers same punishment as repeatMode/repeatDuration, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'repeatPatternLife',
registry.PositiveInteger(300, """duration in seconds before automated patterns are removed"""))
conf.registerChannelValue(ChanTracker, 'shareComputedPatternID',
registry.Integer(-1, """share computed patterns across channels using the same ID, -1 to disable"""))
registry.Probability(0.85,"""percent of similarity needed between previous and current message to trigger a repeat count"""))
conf.registerChannelValue(ChanTracker, 'repeatMode',
registry.String('q',"""mode used by the bot when repeat detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'repeatDuration',
registry.PositiveInteger(180,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'repeatComment',
registry.String('repeat detected',"""comment added on mode changes database, empty for no comment"""))
# mass repeat detection
conf.registerChannelValue(ChanTracker, 'massRepeatChars',
registry.PositiveInteger(40,"""number of chars needed to enter massRepeat detection"""))
conf.registerChannelValue(ChanTracker, 'massRepeatPermit',
registry.Integer(-1,"""Number of repeated text allowed, -1 to disable, tracks message repetition from various sources on the given channel"""))
conf.registerChannelValue(ChanTracker, 'massRepeatLife',
registry.PositiveInteger(12,"""Duration of messages's life in massRepeat counter, in seconds"""))
conf.registerChannelValue(ChanTracker, 'massRepeatPercent',
registry.Probability(0.85,"""percentage similarity between previous and current message to trigger a repeat count"""))
conf.registerChannelValue(ChanTracker, 'massRepeatMode',
registry.String('b',"""mode used by the bot when repeat detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'massRepeatDuration',
registry.PositiveInteger(1800,"""punition in seconds"""))
conf.registerChannelValue(ChanTracker, 'massRepeatComment',
registry.String('mass repeat detected',"""comment added on mode changes database, empty for no comment"""))
conf.registerChannelValue(ChanTracker, 'massRepeatPatternLife',
registry.PositiveInteger(300,"""duration of pattern life"""))
conf.registerChannelValue(ChanTracker, 'massRepeatPatternLength',
registry.Integer(-1,"""if -1, it uses the default system to compare strings, otherwise, it try to find the longest common message, and use it as a regexp pattern,
if found string < length setted, it uses the default string compare"""))
# YES IT'S ANNOYING
conf.registerChannelValue(ChanTracker, 'capPermit',
registry.Integer(-1, """number of UPPERCASE messages allowed, -1 to disable; see capPercent for definition of an UPPERCASE message"""))
registry.Integer(-1,"""Number of UPPERCASE messages allowed, -1 to disable. see capPercent for definition of an UPPERCASE message"""))
conf.registerChannelValue(ChanTracker, 'capLife',
registry.PositiveInteger(30, """duration in seconds before messages are removed from counter"""))
registry.PositiveInteger(30,"""Duration in seconds before messages are removed from count"""))
conf.registerChannelValue(ChanTracker, 'capPercent',
registry.Probability(0.75, """percentage of uppercase chars in a message to trigger a cap count"""))
registry.Probability(0.75,"""percentage of uppercase chars in a message to trigger a cap count"""))
conf.registerChannelValue(ChanTracker, 'capMode',
registry.String('q', """mode used by the bot when cap detection is triggered"""))
registry.String('q',"""mode used by the bot when cap is triggered"""))
conf.registerChannelValue(ChanTracker, 'capDuration',
registry.PositiveInteger(180, """punishment duration in seconds"""))
registry.PositiveInteger(180,"""punition in seconds"""))
conf.registerChannelValue(ChanTracker, 'capComment',
registry.String('capslock detected', """comment added to tracking database, empty for no comment"""))
# hilights
registry.String('capslock detected',"""comment added on mode changes database, empty for no comment"""))
# hilight
conf.registerChannelValue(ChanTracker, 'hilightPermit',
registry.Integer(-1, """number of nicks allowed per message, -1 to disable; note it doesn't care if it's the same nick"""))
registry.Integer(-1,"""Number of nick allowed per message, -1 to disable, note : it doesn't care if it's the same nick"""))
conf.registerChannelValue(ChanTracker, 'hilightMode',
registry.String('q', """mode used by the bot when hilight detection is triggered"""))
registry.String('q',"""mode used by the bot when hilight is triggered"""))
conf.registerChannelValue(ChanTracker, 'hilightDuration',
registry.PositiveInteger(180, """punishment duration in seconds"""))
registry.PositiveInteger(180,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'hilightComment',
registry.String('hilight detected', """comment added to tracking database, empty for no comment"""))
# notices
registry.String('hilight detected',"""comment added on mode changes database, empty for no comment"""))
# channel's notices
conf.registerChannelValue(ChanTracker, 'noticePermit',
registry.Integer(-1, """number of messages allowed, -1 to disable, advice 0"""))
registry.Integer(-1,"""Number of messages allowed, -1 to disable, advice 0"""))
conf.registerChannelValue(ChanTracker, 'noticeLife',
registry.PositiveInteger(3, """duration in seconds before messages are removed from counter"""))
registry.PositiveInteger(3,"""Duration in seconds before messages are removed from count"""))
conf.registerChannelValue(ChanTracker, 'noticeMode',
registry.String('q', """mode used by the bot when notice detection is triggered"""))
registry.String('q',"""mode used by the bot when notice is triggered"""))
conf.registerChannelValue(ChanTracker, 'noticeDuration',
registry.PositiveInteger(300, """punishment duration in seconds"""))
registry.PositiveInteger(300,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'noticeComment',
registry.String('notice detected', """comment added to tracking database, empty for no comment"""))
# ctcps
registry.String('notice detected',"""comment added on mode changes database, empty for no comment"""))
# channel ctcps
conf.registerChannelValue(ChanTracker, 'ctcpPermit',
registry.Integer(-1, """number of messages allowed, -1 to disable"""))
registry.Integer(-1,"""Number of messages allowed, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'ctcpLife',
registry.PositiveInteger(3, """duration in seconds before messages are removed from counter"""))
registry.PositiveInteger(3,"""Duration in seconds before messages are removed from count"""))
conf.registerChannelValue(ChanTracker, 'ctcpMode',
registry.String('b', """mode used by the bot when ctcp detection is triggered"""))
registry.String('b',"""mode used by the bot when ctcp detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'ctcpDuration',
registry.PositiveInteger(1800, """punishment duration in seconds"""))
registry.PositiveInteger(1800,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'ctcpComment',
registry.String('ctcp detected', """comment added to tracking database, empty for no comment"""))
# join/part flood
registry.String('ctcp detected',"""comment added on mode changes database, empty for no comment"""))
# channel join/part flood
conf.registerChannelValue(ChanTracker, 'cyclePermit',
registry.Integer(-1, """number of cycles allowed, -1 to disable; counts parts and quits"""))
registry.Integer(-1,"""Number of cycles allowed, -1 to disable, count part and quit"""))
conf.registerChannelValue(ChanTracker, 'cycleLife',
registry.PositiveInteger(180, """duration in seconds before cycles are removed from counter"""))
registry.PositiveInteger(180,"""Duration in seconds before cycles are removed from count"""))
conf.registerChannelValue(ChanTracker, 'cycleMode',
registry.String('b', """mode used by the bot when cycle detection is triggered"""))
registry.String('b',"""mode used by the bot when cycle detection is triggered"""))
conf.registerChannelValue(ChanTracker, 'cycleDuration',
registry.PositiveInteger(1800, """punishment duration in seconds"""))
registry.PositiveInteger(1800,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'cycleComment',
registry.String('cycle detected', """comment added to tracking database, empty for no comment"""))
registry.String('cycle detected',"""comment added on mode changes database, empty for no comment"""))
conf.registerChannelValue(ChanTracker, 'cycleForward',
registry.String('', """if your ircd supports that, you can forward the user to a specific channel"""))
# massjoin from a host
registry.String('',"""if your ircd supports that, you can forward the user to a specific channel"""))
# channel massJoin from an host
conf.registerChannelValue(ChanTracker, 'massJoinPermit',
registry.Integer(-1, """number of joins allowed, -1 to disable, note, it could mixup a bit with cycle detection"""))
registry.Integer(-1,"""Number of joins allowed, -1 to disable, note, it could mixup a bit with cycle detection"""))
conf.registerChannelValue(ChanTracker, 'massJoinLife',
registry.PositiveInteger(60, """duration in seconds before messages are removed from count"""))
registry.PositiveInteger(60,"""Duration in seconds before messages are removed from count"""))
conf.registerChannelValue(ChanTracker, 'massJoinMode',
registry.String('+rq-z $~a', """mode used by the bot when massjoin detection is triggered"""))
registry.String('+rq-z $~a',"""mode used by the bot when massjoin is triggered"""))
conf.registerChannelValue(ChanTracker, 'massJoinDuration',
registry.PositiveInteger(300, """punishment duration in seconds"""))
registry.PositiveInteger(300,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'massJoinUnMode',
registry.String('-rq+z $~a', """mode used by the bot when massJoinDuration is finished"""))
registry.String('-rq+z $~a',"""mode used by the bot when massJoinDuration is finished"""))
# nick changes flood
conf.registerChannelValue(ChanTracker, 'nickPermit',
registry.Integer(-1, """number of nick changes allowed, -1 to disable"""))
registry.Integer(-1,"""Number of nick changes allowed, -1 to disable"""))
conf.registerChannelValue(ChanTracker, 'nickLife',
registry.PositiveInteger(300, """duration in seconds before nick changes are removed from count"""))
registry.Integer(300,"""Duration in seconds before nick changes are removed from count"""))
conf.registerChannelValue(ChanTracker, 'nickMode',
registry.String('q', """mode used by the bot when nick detection is triggered"""))
registry.String('q',"""mode used by the bot when nick is triggered"""))
conf.registerChannelValue(ChanTracker, 'nickDuration',
registry.PositiveInteger(300, """punishment duration in seconds"""))
registry.PositiveInteger(300,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'nickComment',
registry.String('nick changes flood detected', """comment added to tracking database, empty for no comment"""))
# if you enable this, each time someone triggers other protection, it increases this counter
registry.String('nick changes flood detected',"""comment added on mode changes database, empty for no comment"""))
# if you enable this, each time someone trigger other protection that will increase this queue
conf.registerChannelValue(ChanTracker, 'badPermit',
registry.Integer(-1, """number of actions allowed, -1 to disable; each time bot had to act on a user, it increases this counter"""))
registry.Integer(-1,"""Number of bad action allowed, -1 to disable, each time bot had to acts on a user, it increase this item"""))
conf.registerChannelValue(ChanTracker, 'badLife',
registry.PositiveInteger(600, """duration in seconds before actions are removed from counter"""))
registry.Integer(600,"""Duration in seconds before actions are removed from count"""))
conf.registerChannelValue(ChanTracker, 'badMode',
registry.String('b', """mode used by the bot when bad detection is triggered"""))
registry.String('b',"""mode used by the bot when bad is triggered"""))
conf.registerChannelValue(ChanTracker, 'badDuration',
registry.PositiveInteger(86400, """punishment duration in seconds"""))
registry.PositiveInteger(86400,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'badComment',
registry.String('bad detected', """comment added to tracking database, empty for no comment"""))
# if you enable this, each time someone triggers bad detection, it increases this counter
registry.String('bad detected',"""comment added on mode changes database, empty for no comment"""))
# if you enable this, each time someone trigger bad in a channel that will increase this queue
conf.registerChannelValue(ChanTracker, 'attackPermit',
registry.Integer(-1, """number of bad actions allowed, -1 to disable; each time bot flags a user as bad, it increases this counter"""))
registry.Integer(-1,"""Number of bad action allowed, -1 to disable, each time bot flags user as bad, it increase this item"""))
conf.registerChannelValue(ChanTracker, 'attackLife',
registry.PositiveInteger(600, """duration in seconds before bad actions are removed from counter"""))
registry.Integer(600,"""Duration in seconds before actions are removed from count"""))
conf.registerChannelValue(ChanTracker, 'attackDuration',
registry.PositiveInteger(1800, """punishment duration in seconds"""))
registry.PositiveInteger(1800,"""punishment duration in seconds"""))
conf.registerChannelValue(ChanTracker, 'attackMode',
registry.String('+rq-z $~a', """mode used by the bot when attack detection is triggered"""))
registry.String('+rq-z $~a',"""mode used by the bot when attack is triggered"""))
conf.registerChannelValue(ChanTracker, 'attackUnMode',
registry.String('-rq+z $~a', """mode used by the bot when attackDuration is finished"""))
# netsplits
registry.String('-rq+z $~a',"""mode used by the bot when attackDuration is finished"""))
conf.registerChannelValue(ChanTracker, 'netsplitModes',
registry.String('', """leave empty for no modes changes"""))
registry.String('',"""leave empty for no modes changes"""))
conf.registerChannelValue(ChanTracker, 'netsplitUnmodes',
registry.String('', """leave empty for no modes changes"""))
registry.String('',"""leave empty for no modes changes"""))
conf.registerChannelValue(ChanTracker, 'netsplitDuration',
registry.PositiveInteger(600, """duration of netsplit state when detected,
it disables massjoin and cycle detection, and could set specific modes"""))
registry.PositiveInteger(600,"""duration of netsplit state when detected, it disables massJoin and cycle detection, and could set specific modes"""))

8194
plugin.py

File diff suppressed because it is too large Load Diff

623
server.py
View File

@ -1,68 +1,76 @@
import os, re, time, base64
import BaseHTTPServer
import os
import time
import base64
import re
import supybot.utils as utils
import http.server, sqlite3
import sqlite3
import collections
import urllib
from StringIO import StringIO
host = 'http://domain.tld'
port = 80
standalone = True
webpath = '/bantracker'
username = 'username'
password = 'password'
filename = '/home/botaccount/data/networkname/ChanTracker.db'
channels = [] # empty to allow view of all channels recorded, otherwise restrict the views to channels
# httpd server address
if not standalone:
servaddr = '127.0.0.1'
else:
servaddr = ''
channels = [] # empty to allows view of all channels recorded, otherwise restrict the views to channels
# usage python server.py
auth = '%s:%s' % (username,password)
base64string = base64.b64encode(auth.encode('utf-8')).decode('utf-8')
def weblink():
weblink = host
if standalone:
weblink += ':%s' % port
base64string = base64.encodestring('%s:%s' % (username,password))[:-1]
def timeElapsed(elapsed, short=False, leadingZeroes=False, years=True,
weeks=True, days=True, hours=True, minutes=True, seconds=True):
"""Given <elapsed> seconds, returns a string with an English description of
how much time as passed. leadingZeroes determines whether 0 days, 0 hours,
etc. will be printed; the others determine what larger time periods should
be used.
"""
ret = []
def Format(s, i):
if i or leadingZeroes or ret:
if short:
ret.append('%s%s' % (i, s[0]))
else:
ret.append(format('%n', (i, s)))
elapsed = int(elapsed)
assert years or weeks or days or \
hours or minutes or seconds, 'One flag must be True'
if years:
(yrs, elapsed) = (elapsed // 31536000, elapsed % 31536000)
Format('year', yrs)
if weeks:
(wks, elapsed) = (elapsed // 604800, elapsed % 604800)
Format('week', wks)
if days:
(ds, elapsed) = (elapsed // 86400, elapsed % 86400)
Format('day', ds)
if hours:
(hrs, elapsed) = (elapsed // 3600, elapsed % 3600)
Format('hour', hrs)
if minutes or seconds:
(mins, secs) = (elapsed // 60, elapsed % 60)
if leadingZeroes or mins:
Format('minute', mins)
if seconds:
leadingZeroes = True
Format('second', secs)
if not ret:
raise ValueError, 'Time difference not great enough to be noted.'
if short:
return ' '.join(ret)
else:
weblink += webpath
weblink += '/?hash=%s' % base64string
return weblink
return format('%L', ret)
def htmlEscape(text):
return text.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
class MyHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
server_version= "Ircd-Seven/1.1"
def do_GET( self ):
self.page( self.path )
class BanTracker(http.server.BaseHTTPRequestHandler):
if not standalone:
def log_request(self, *args):
pass # disable logging
def do_GET(self):
self.page(self.path)
def page(self, query):
def write(subtitle, body):
page = [
'<!DOCTYPE html>', '<html>', '<head>',
'<title>BanTracker%s</title>' % (' &raquo; %s' % subtitle if subtitle else ''),
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
'<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />',
'</head>', '<body style="margin:0.5em; width:98%;" class="container">'
] + body + ['</body>', '</html>']
self.send_response(200)
self.send_header("Content-Type", "text/html")
full = '\n'.join(page)
print('HTML lines %s' % len(full))
self.send_header("Content-Length", len(full))
self.end_headers()
self.wfile.write(full.encode('utf-8'))
if standalone:
h = '%s:%s/' % (host,port)
else:
h = '%s/' % webpath
def page (self,query):
h = '%s:%s/' % (host,port)
body = []
if not query:
return
if query.startswith('/?username='):
@ -76,260 +84,287 @@ class BanTracker(http.server.BaseHTTPRequestHandler):
if aa[0] == 'password':
p = aa[1]
if u and p:
auth = '%s:%s' % (u,p)
raw = base64.b64encode(auth.encode('utf-8')).decode('utf-8')
if raw != base64string:
raw = base64.encodestring('%s:%s' % (u,p))[:-1]
if not raw == base64string:
query = ''
else:
query = '/?hash=%s' % base64string
if not query.startswith('/?hash='):
subtitle = ''
body = [
'<form action="%s">' % h,
'<p>Username: <input name="username" /></p>',
'<p>Password: <input name="password" type="password" /></p>',
'<button type="submit" class="btn btn-default">Login</button>',
'</form>'
]
write(subtitle, body)
body.append('<html>\n<head>\n<title>ChanTracker</title>\n')
body.append('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n')
body.append("</head>\n<body>\n")
body.append('<form action="%s">\n' % h)
body.append('<p>Username:<input name="username" /></p>\n')
body.append('<p>Password:<input name="password" type="password"/></p>\n')
body.append('<input type="submit" value="Login" />\n')
body.append("</form>\n")
body.append("</body>\n<html>\n")
self.send_response(200)
self.send_header("Content-type","text/html")
full = ''.join(body)
self.send_header("Content-length",str(len(full)))
self.end_headers()
self.wfile.write(full)
return
query = query.replace('%3D','=')
query = query.replace('/?hash=%s' % base64string,'')
query = query.lstrip('&')
q = '?hash=%s' % base64string
query = utils.web.urlunquote(query)
subtitle = ''
body = [
'<div class="row"><div class="col-xs-6" style="width:100%; max-width:600px;">',
'<form action="%s" class="form">' % q,
'<div class="input-group">',
'<input type="hidden" name="hash" value="%s">' % base64string,
'<input name="search" class="form-control" />',
'<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>',
'</div></form></div></div>',
'<div class="clearfix"></div>'
]
if not query:
write(subtitle, body)
return
print(query)
subtitle = query
if query.startswith('/?hash='):
a = query.split('&')[0]
a = a.replace('/?hash=','')
query = query.replace('%3D','=')
query = query.replace('/?hash=%s' % base64string,'/')
q = '?hash=%s' % base64string
query = urllib.unquote( query )
print query
body.append('<html style="text-align:center;font-size:1.2em;">\n<head>\n<title>BanTracker - %s</title>\n' % query)
body.append('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n')
body.append('<link rel="stylesheet" href="http://getbootstrap.com/dist/css/bootstrap.min.css"></link>\n')
# body.append('<script src="http://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>\n')
body.append('</head>\n<body style="margin:0.5em;width:98%;margin-left:auto;margin-right:auto;text-align:left;" class="container">\n')
body.append('<div class="row"><div class="col-xs-6">\n')
body.append('<form action="%s" class="form">\n' % q)
body.append('<div class="input-group">')
body.append('<input type="hidden" name="hash" value="%s">' % base64string)
body.append('<input name="search" class="form-control" />\n')
body.append('<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>\n')
body.append('</div></form></div></div>\n')
body.append('<div class="clearfix"></div>\n')
db = self._getbandb()
c = db.cursor()
ar = []
if query.startswith('id='):
search = query.split('=')[1]
si = int(search)
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=?""",(si,))
r = c.fetchall()
if len(r):
ban = r[0]
(bid,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
if not channels or channel in channels:
body.extend([
'<h3>#%d</h3>' % bid,
'<p>#%d by <a href="%s%s&amp;%s">%s</a>' % (bid,h,q,utils.web.urlencode({'oper':oper}),oper),
'in <a href="%s%s&amp;channel=%s">%s</a>:' % (h,q,channel.split('#')[1],channel),
'+%s <a href="%s%s&amp;%s">%s</a></p>' % (kind,h,q,utils.web.urlencode({'mask':mask}),mask),
'<p>Begin at %s</p>' % time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(begin_at)))
])
was = float(begin_at) == float(end_at)
if was:
was = 'forever'
else:
was = utils.timeElapsed(float(end_at) - float(begin_at))
body.append('<p>Original duration: %s</p>' % was)
if not removed_at:
if was != 'forever':
remaining = float(end_at) - time.time()
if remaining >= 0:
body.append('<p>It will expire in %s</p>' % utils.timeElapsed(remaining))
else:
body.append('<p>It expired %s</p>' % utils.timeElapsed(remaining))
else:
body.extend(['<p>Removed after %s' % utils.timeElapsed(float(removed_at)-float(begin_at)),
'on %s' % time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(removed_at))),
'by <a href="%s%s&amp;%s">%s</a></p>' % (h,q,utils.web.urlencode({'removed_by':removed_by}),removed_by)])
c.execute("""SELECT full,log FROM nicks WHERE ban_id=?""",(bid,))
r = c.fetchall()
if len(r):
body.append('<h3>Logs</h3>')
for (full,log) in r:
body.append('<p>for %s</p>' % full)
if log != '':
body.append('<ul>')
for line in log.split('\n'):
if line != '':
body.append('<li>%s</li>' % htmlEscape(line))
body.append('</ul>')
c.execute("""SELECT oper,at,comment FROM comments WHERE ban_id=?""",(bid,))
r = c.fetchall()
if len(r):
body.extend(['<h3>Comments</h3>', '<ul>'])
for (oper,at,com) in r:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(at)))
body.append('<li>%s by %s: %s</li>' % (s,oper,htmlEscape(com)))
body.append('</ul>')
c.close()
write(subtitle, body)
return
elif query.startswith('channel='):
search = '#'+query.split('=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE channel=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
ar.extend(r)
elif query.startswith('removed_by='):
search = query.split('=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE removed_by=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
ar.extend(r)
elif query.startswith('oper='):
search = query.split('=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE oper=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
ar.extend(r)
elif query.startswith('mask='):
search = query.split('=')[1]
sg = '*%s*' % search
sl = '%%%s%%' % search
c.execute("""SELECT ban_id,full FROM nicks WHERE full GLOB ? OR full LIKE ? OR log GLOB ? OR log LIKE ? ORDER BY ban_id DESC""",(sg,sl,sg,sl))
r = c.fetchall()
L = []
a = {}
if len(r):
d = []
for (bid,full) in r:
if bid not in d:
d.append(bid)
for bid in d:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=?""",(bid,))
r = c.fetchall()
if len(r):
for ban in r:
a[ban[0]] = ban
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE mask GLOB ? OR mask LIKE ? ORDER BY id DESC""",(sg,sl))
r = c.fetchall()
if len(r):
for ban in r:
a[ban[0]] = ban
if len(a):
ar = []
for ban in list(a.keys()):
ar.append(a[ban])
ar.sort(key=lambda x: x[0], reverse=True)
elif query.startswith('search='):
search = query.split('=')[1]
search = search.replace('+','*')
print(search)
if search:
if not re.match(r'^[0-9]+$', search):
sg = '*%s*' % search
sl = '%%%s%%' % search
si = None
c.execute("""SELECT ban_id,full FROM nicks WHERE full GLOB ? OR full LIKE ? OR log GLOB ? OR log LIKE ? ORDER BY ban_id DESC""",(sg,sl,sg,sl))
r = c.fetchall()
else:
si = int(search)
r = []
L = []
a = {}
if len(r):
d = []
for (bid,full) in r:
if bid not in d:
d.append(bid)
for bid in d:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=?""",(bid,))
r = c.fetchall()
if len(r):
for ban in r:
a[ban[0]] = ban
if not si:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE mask GLOB ? OR mask LIKE ? OR channel GLOB ? OR channel LIKE ? OR oper GLOB ? OR oper LIKE ? ORDER BY id DESC""",(sg,sl,sg,sl,sg,sl))
else:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=?""",(si,))
if query:
ar = []
if query.startswith('/&id='):
search = query.split('/&id=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
for ban in r:
a[ban[0]] = ban
if not si:
c.execute("""SELECT ban_id, comment FROM comments WHERE comment GLOB ? OR comment LIKE ? ORDER BY ban_id DESC""",(sg,sl))
r = c.fetchall()
else:
r = []
ban = r[0]
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
if not len(channels) or channel in channels:
body.append('<h3>#%s</h3>\n' % id)
body.append('<p>#%s by %s in %s : +%s : %s</p>\n' % (id,oper,channel,kind,mask))
body.append('<p>Begin at %s</p>\n' % time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(begin_at))))
was = float(begin_at) == float(end_at)
if was:
was = 'forever'
else:
was = timeElapsed(float(end_at) - float(begin_at))
body.append('<p>Original duration : %s</p>\n' % was)
if not removed_at:
if was != 'forever':
body.append('<p>%s</p>\n' % 'It will expire in %s' % timeElapsed(float(end_at) - time.time()))
else:
body.append('<p>%s</p>\n' % 'Removed after %s on %s by %s' % (timeElapsed(float(removed_at)-float(begin_at)),time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(removed_at))),removed_by))
c.execute("""SELECT full, log FROM nicks WHERE ban_id=?""",(id,))
r = c.fetchall()
if len(r):
users = r
body.append('<h3>Logs</h3>\n')
for u in users:
(full,log) = u
body.append('<p>for %s</p>\n' % full)
if log != '':
body.append('<ul>\n')
for line in log.split('\n'):
if line != '':
body.append('<li>%s</li>\n' % line)
body.append('</ul>\n')
c.execute("""SELECT oper, at, comment FROM comments WHERE ban_id=?""",(id,))
r = c.fetchall()
if len(r):
body.append('<h3>Comments</h3>\n')
body.append('<ul>\n')
comments = r
for com in comments:
(oper,at,comment) = com
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(at)))
body.append('<li>%s by %s : %s</li>\n' % (s,oper,comment))
body.append('</ul>\n')
elif query.startswith('/&channel='):
search = '#'+query.split('/&channel=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE channel=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
d = []
for (bid,full) in r:
d.append(bid)
for bid in d:
if bid not in a:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=?""",(bid,))
r = c.fetchall()
if len(r):
for ban in r:
a[ban[0]] = ban
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
ar.append([int(id),channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by])
elif query.startswith('/&removed_by='):
search = query.split('/&removed_by=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE removed_by=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
ar.append([int(id),channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by])
elif query.startswith('/&oper='):
search = query.split('/&oper=')[1]
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE oper=? ORDER BY id DESC""",(search,))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
ar.append([int(id),channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by])
elif query.startswith('/&mask='):
search = query.split('/&mask=')[1]
glob = '*%s*' % search
like = '%'+search+'%'
c.execute("""SELECT ban_id, full FROM nicks WHERE full GLOB ? OR full LIKE ? OR log GLOB ? OR log LIKE ? ORDER BY ban_id DESC""",(glob,like,glob,like))
L = []
a = {}
r = c.fetchall()
if len(r):
bans = r
d = {}
for ban in bans:
(id,full) = ban
if not id in d:
d[id] = id
for id in d:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=? ORDER BY id DESC""",(int(id),))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
a[str(id)] = ban
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE mask GLOB ? OR mask LIKE ? ORDER BY id DESC""",(glob,like))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
a[str(id)] = ban
if len(a):
ar = []
for ban in list(a.keys()):
ar.append(a[ban])
ar.sort(key=lambda x: x[0], reverse=True)
if len(ar):
print('Found %s results' % len(ar))
body.extend([
'<h3>Results <small>%s</small></h3>' % search,
'<div class="row"><div class="col-xs-12"><table class="table table-bordered">',
'<thead><tr><th>ID</th><th>Channel</th><th>Operator</th><th>Type</th><th>Mask</th><th>Begin date</th><th>End date</th><th>Removed</th><th>Removed by</th></tr></thead>',
'<tbody>'
])
for ban in ar:
(bid,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
if not channels or channel in channels:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(begin_at)))
body.extend([
'<tr>',
'<td><a href="%s%s&amp;id=%d">%d</a></td>' % (h,q,bid,bid),
'<td><a href="%s%s&amp;channel=%s">%s</a></td>' % (h,q,channel.split('#')[1],channel),
'<td><a href="%s%s&amp;%s">%s</a></td>' % (h,q,utils.web.urlencode({'oper':oper}),oper),
'<td>+%s</td>' % kind,
'<td><a href="%s%s&amp;%s">%s</a></td>' % (h,q,utils.web.urlencode({'mask':mask}),mask),
'<td>%s</td>' % s
])
if end_at and end_at != begin_at:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(end_at)))
body.append('<td>%s</td>' % s)
else:
body.append('<td></td>')
if removed_at:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(removed_at)))
body.append('<td>%s</td>' % s)
else:
body.append('<td></td>')
if removed_by:
body.append('<td><a href="%s%s&amp;%s">%s</a></td>' % (h,q,utils.web.urlencode({'removed_by':removed_by}),removed_by))
else:
body.append('<td></td>')
# affected = ''
# try:
# c.execute("""SELECT full, log FROM nicks WHERE ban_id=?""",(bid,))
# affected = len(c.fetchall())
# except:
for ban in a:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = a[ban]
ar.append([int(id),channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by])
def sort_function (item):
return item[0]
ar.sort(key=sort_function)
ar.sort(reverse=True)
elif query.startswith('/&search='):
search = query.split('/&search=')[1]
search = search.replace('+','*')
print search
if search:
s = '*%s*' % search
qu = '%'+search+'%'
c.execute("""SELECT ban_id, full FROM nicks WHERE full GLOB ? OR full LIKE ? OR log GLOB ? OR log LIKE ? ORDER BY ban_id DESC""",(s,qu,s,qu))
L = []
a = {}
r = c.fetchall()
if len(r):
bans = r
d = {}
for ban in bans:
(id,full) = ban
if not id in d:
d[id] = id
for id in d:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=? ORDER BY id DESC""",(int(id),))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
a[id] = ban
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE mask GLOB ? OR mask LIKE ? OR channel GLOB ? OR channel LIKE ? OR oper GLOB ? OR oper LIKE ? ORDER BY id DESC""",(s,qu,s,qu,s,qu))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
a[id] = ban
c.execute("""SELECT ban_id, comment FROM comments WHERE comment GLOB ? OR comment LIKE ? ORDER BY ban_id DESC""",(s,qu))
r = c.fetchall()
d = {}
if len(r):
bans = r
for ban in bans:
(id,full) = ban
d[id] = id
for id in d:
if not id in a:
c.execute("""SELECT id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by FROM bans WHERE id=? ORDER BY id DESC LIMIT 1""",(int(id),))
r = c.fetchall()
if len(r):
bans = r
for ban in bans:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ban
a[id] = ban
if len(a):
ar = []
for ban in a:
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = a[ban]
ar.append([int(id),channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by])
def sort_function (item):
return item[0]
ar.sort(key=sort_function)
ar.sort(reverse=True)
else:
body.append('<p>nothing found</p>\n')
if len(ar):
print 'found %s results' % len(ar)
i = 0
body.append('<h3>results <small>%s</small></h3>' % search)
body.append('<div class="row"><div class="col-xs-12"><table class="table table-bordered sortable">\n')
body.append('<thead><tr><th>ID</th><th>Channel</th><th>Operator</th><th>Kind</th><th>Target</th><th>Begin date</th><th>End date</th><th>Removed date</th><th>Removed by</th></tr></thead>\n')
body.append('<tbody>\n')
while i < len(ar):
(id,channel,oper,kind,mask,begin_at,end_at,removed_at,removed_by) = ar[i]
if not len(channels) or channel in channels:
body.append('<tr>\n')
body.append('<td><a href="%s%s&id=%s">%s</a></td>\n' % (h,q,id,id))
body.append('<td><a href="%s%s&channel=%s">%s</a></td>\n' % (h,q,channel.split('#')[1],channel))
body.append('<td><a href="%s%s&%s">%s</a></td>\n' % (h,q,urllib.urlencode({'oper':oper}),oper))
body.append('<td>+%s</td>\n' % kind)
body.append('<td><a href="%s%s&%s">%s</a></td>\n' % (h,q,urllib.urlencode({'mask':mask}),mask))
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(begin_at)))
body.append('<td>%s</td>\n' % s)
if end_at and end_at != begin_at:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(end_at)))
body.append( '<td>%s</td>\n' % s)
else:
body.append( '<td></td>')
if removed_at:
s = time.strftime('%Y-%m-%d %H:%M:%S GMT',time.gmtime(float(removed_at)))
body.append( '<td>%s</td>' % s)
else:
body.append( '<td></td>\n' )
if removed_by:
body.append( '<td><a href="%s%s&%s">%s</a></td>\n' % (h,q,urllib.urlencode({'removed_by':removed_by}),removed_by))
else:
body.append( '<td></td>\n')
# affected = ''
# body.append('<td>%s</td>' % affected)
body.append('</tr>')
body.extend(['</tbody>', '</table></div>'])
else:
body.append('<p>Nothing found</p>')
# try:
# c.execute("""SELECT full, log FROM nicks WHERE ban_id=?""",(id,))
# affected = len(c.fetchall())
# except:
# affected = ''
# body.append( '<td>%s</td>\n' % affected)
body.append( '</tr>\n')
i = i+1
body.append('</tbody>\n')
body.append('</table></div>\n')
body.append("</body></html>")
self.send_response(200)
self.send_header("Content-type","text/html")
full = ''.join(body)
print 'html lines %s' % len(full)
self.send_header("Content-length",len(full))
self.end_headers()
self.wfile.write(full)
c.close()
write(subtitle, body)
def _getbandb(self):
def _getbandb (self):
if os.path.exists(filename):
db = sqlite3.connect(filename,timeout=10)
db.text_factory = str
return db
db = sqlite3.connect(filename)
db.text_factory = str
c = db.cursor()
c.execute("""CREATE TABLE bans (
id INTEGER PRIMARY KEY,
@ -343,24 +378,24 @@ class BanTracker(http.server.BaseHTTPRequestHandler):
removed_by VARCHAR(1000)
)""")
c.execute("""CREATE TABLE nicks (
ban_id INTEGER,
ban_id INTEGER,
ban VARCHAR(1000) NOT NULL,
full VARCHAR(1000) NOT NULL,
log TEXT NOT NULL
log TEXT NOT NULL
)""")
c.execute("""CREATE TABLE comments (
ban_id INTEGER,
oper VARCHAR(1000) NOT NULL,
oper VARCHAR(1000) NOT NULL,
at TIMESTAMP NOT NULL,
comment TEXT NOT NULL
)""")
db.commit()
return db
def httpd(handler_class=BanTracker, server_address=(servaddr, port)):
srvr = http.server.HTTPServer(server_address, handler_class)
def httpd(handler_class=MyHandler, server_address = ('', port), ):
srvr = BaseHTTPServer.HTTPServer(server_address, handler_class)
srvr.serve_forever()
if __name__ == "__main__":
httpd()
httpd( )

View File

@ -1,13 +0,0 @@
import os
import sys
from supybot.setup import plugin_setup
# Workaround pip changing the name of the root directory
(parent, dir_) = os.path.split(os.path.dirname(__file__))
sys.path.insert(0, parent)
sys.modules["ChanTracker"] = __import__(dir_)
plugin_setup(
'ChanTracker',
)

View File

@ -25,6 +25,7 @@
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *