mirror of
https://github.com/ncoevoet/ChanTracker.git
synced 2025-04-26 13:01:06 -05:00
Compare commits
210 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4d1e5e6b53 | ||
|
acde82892a | ||
|
d2d25f94d2 | ||
|
10a1b41c47 | ||
|
e5ce04c99b | ||
|
41a9bd5eaa | ||
|
f0dce70427 | ||
|
e50ca6fe0d | ||
|
992406118b | ||
|
bbd7ace6ca | ||
|
546cbab98d | ||
|
724f4126ef | ||
|
0ff2476e87 | ||
|
7fe15a15d8 | ||
|
fc00cf9a3b | ||
|
fe6ce22908 | ||
|
2646838ad7 | ||
|
4c53d28f11 | ||
|
0d257df7d6 | ||
|
a2aa1fa9ea | ||
|
278ea99e51 | ||
|
3931b790d5 | ||
|
67b334a431 | ||
|
bf3f2ef52d | ||
|
307908fc99 | ||
|
e552313951 | ||
|
2c94abb49a | ||
|
f02c24868a | ||
|
a62ac36f62 | ||
|
065d698b31 | ||
|
4f320388dd | ||
|
789e01a28a | ||
|
2990b35f13 | ||
|
11abee0247 | ||
|
d8acb0b785 | ||
|
051a76fa2e | ||
|
5231ed21a8 | ||
|
f578c2821f | ||
|
dbf84cbf1b | ||
|
e43d2c999e | ||
|
9c53e66bf8 | ||
|
3ef7520394 | ||
|
aca5b627d9 | ||
|
cbe0f54a16 | ||
|
1053a2975b | ||
|
b097d35669 | ||
|
cb6dc92a85 | ||
|
40098356be | ||
|
6d44e3185a | ||
|
1844e63234 | ||
|
8d0d64e843 | ||
|
dc0615a29a | ||
|
817353d6dd | ||
|
2fcae101ce | ||
|
81e8093cd3 | ||
|
7af31bc213 | ||
|
a6a22bfe2b | ||
|
3108d60f3b | ||
|
a4abc5c506 | ||
|
e0c606816d | ||
|
b53dbb8dd2 | ||
|
eef26588c9 | ||
|
d7b888b8eb | ||
|
8250443443 | ||
|
e25c845b50 | ||
|
8c1912b1a1 | ||
|
baa2bfc48f | ||
|
408b449f62 | ||
|
7bced17838 | ||
|
0659919ad2 | ||
|
96ea85c3b9 | ||
|
0b24db1311 | ||
|
3347a755bd | ||
|
b93c11e7f5 | ||
|
fc6d1fa64b | ||
|
63ea755d8f | ||
|
b98455e798 | ||
|
2f7b3c548a | ||
|
ef3a7973ba | ||
|
be7cb231dd | ||
|
70e98f2c74 | ||
|
922e6b83ab | ||
|
af16cbf9de | ||
|
9c41bf5466 | ||
|
4bcfb7d7c2 | ||
|
980d067bf3 | ||
|
7de2ddf161 | ||
|
f4cfdf9722 | ||
|
2c69f1bc05 | ||
|
17010410e1 | ||
|
0b0b496b49 | ||
|
b5d03d9718 | ||
|
80fa0b9672 | ||
|
f088cea7f6 | ||
|
f00be5916c | ||
|
15475ec507 | ||
|
38ab2e449f | ||
|
625761a555 | ||
|
f6ff5b8128 | ||
|
c92c1ded5f | ||
|
a3d25c44b7 | ||
|
3c80464cdf | ||
|
38e21926d1 | ||
|
b1799be27c | ||
|
22103587fa | ||
|
4b751acb8e | ||
|
270b2c143f | ||
|
6628af1088 | ||
|
74c459dc71 | ||
|
7a60d99121 | ||
|
30afb2817c | ||
|
05f4297c54 | ||
|
a456d4e431 | ||
|
33dd4fdcfe | ||
|
61abdda95a | ||
|
2962c3190b | ||
|
26646c7d7f | ||
|
2060b4f8c6 | ||
|
9ce973e926 | ||
|
762163f44a | ||
|
2407d05aa7 | ||
|
94990c5234 | ||
|
c0bb2d11e8 | ||
|
1068737ed1 | ||
|
25aee3bc91 | ||
|
7c136558f9 | ||
|
d0ff0d36c4 | ||
|
7fd423a7d8 | ||
|
c46ab7297e | ||
|
ea20091803 | ||
|
d182868858 | ||
|
0a09ad0ac9 | ||
|
7b9edfff6b | ||
|
84238bfed9 | ||
|
1aaf30c8fe | ||
|
06013c8aa7 | ||
|
9874005392 | ||
|
668f03f120 | ||
|
3d0a3753f4 | ||
|
d70e44b0b8 | ||
|
0c105a03ba | ||
|
7a4ff48863 | ||
|
02f23df444 | ||
|
903a37b893 | ||
|
88985f3b55 | ||
|
93d55d9052 | ||
|
648c416ca6 | ||
|
2e31eb5983 | ||
|
c33569f42a | ||
|
9278114aed | ||
|
90b968cfb6 | ||
|
560fc423c7 | ||
|
2fe1022861 | ||
|
8a9bfc77fb | ||
|
a13986ec2c | ||
|
1131f3870d | ||
|
a46700b5ef | ||
|
8169616808 | ||
|
3c9900a1b0 | ||
|
cdb8e51190 | ||
|
f943106731 | ||
|
ecaa309c30 | ||
|
31911a8cdd | ||
|
99ed2cd8f3 | ||
|
687597ece8 | ||
|
ca758a0eae | ||
|
d2cf0cf8a6 | ||
|
fd99d11acb | ||
|
5b2e162f70 | ||
|
d17109cda2 | ||
|
4b7873afd2 | ||
|
609044d16c | ||
|
8ee6df5c3d | ||
|
eff74cf932 | ||
|
221ea871ec | ||
|
0bb1f023cb | ||
|
32799a9e7f | ||
|
0f5bacd675 | ||
|
232e962205 | ||
|
3b0b696cc1 | ||
|
ebe7cc8e1f | ||
|
4b20d96858 | ||
|
b83f8cd32f | ||
|
be8a31e29c | ||
|
84ae5729e3 | ||
|
b9a835a0c7 | ||
|
acd292fdda | ||
|
9ade56fa75 | ||
|
705a902598 | ||
|
883bee94a8 | ||
|
a7a83d6e03 | ||
|
ee508947b0 | ||
|
fc7feae30a | ||
|
b1a90e3b9e | ||
|
6ae5ba614c | ||
|
d21c619c0e | ||
|
10a1b8ea87 | ||
|
1bb319b1de | ||
|
8aa34e5bfe | ||
|
fe46de323d | ||
|
2105a15579 | ||
|
a44d6c9610 | ||
|
177c5ff1aa | ||
|
6a7df62354 | ||
|
cfa54dda59 | ||
|
bf6e263e15 | ||
|
a3d7425c66 | ||
|
e8523f6a8f | ||
|
fe5fc2cc71 | ||
|
0fdad9901e |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ncoevoet]
|
400
README.md
400
README.md
@ -1,43 +1,124 @@
|
||||
# ChanTracker : a supybot plugin for ban tracking #
|
||||
# ChanTracker — a Supybot plugin for ban tracking #
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
The plugin is used in various and large channels on freenode and others networks
|
||||
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`.
|
||||
|
||||
## Commands ##
|
||||
|
||||
!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>
|
||||
@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)
|
||||
|
||||
## General Usage ##
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
!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 +b 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 ban set
|
||||
against them (details below).
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
/msg chanserv #myChannel op
|
||||
/mode #myChannel +q *!*@ranty.ian.home
|
||||
@ -46,7 +127,9 @@ Alternatively, the bot can be used just to track the mode changes, with ops usin
|
||||
/kick ham
|
||||
/msg mybigbadbot 30d silly spammer
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
/msg mybigbadbot query ian!*@*
|
||||
/msg mybigbadbot pending #myChannel
|
||||
@ -55,7 +138,8 @@ If you annotate the bans within 3 minutes of setting them, then you can do so wi
|
||||
/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
|
||||
@ -63,188 +147,244 @@ ChanTracker also allows you to work out which users would be affected by a ban b
|
||||
|
||||
## Settings ##
|
||||
|
||||
**If you want the bot to manage its own op status, you must change the config value**:
|
||||
If you want the bot to manage its own op status, you must change this setting:
|
||||
|
||||
!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 the 'doNothingAboutOwnOpStatus' changed to False, bot will deop in each channel is in (if opped) so take a look at:
|
||||
After `doNothingAboutOwnOpStatus` is changed to `False`, the bot will deop in each channel it is opped in, 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 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
|
||||
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:
|
||||
|
||||
!config supybot.protocols.irc.ping.interval 3600
|
||||
@config supybot.protocols.irc.ping.interval 60
|
||||
|
||||
Here list of data requested by the bot at join:
|
||||
Here is the list of data requested by the bot at join:
|
||||
|
||||
JOIN :#channel
|
||||
MODE :#channel
|
||||
MODE :#channel b
|
||||
MODE :#channel q
|
||||
WHO #CHANNEL %tnuhiar,42
|
||||
WHO #CHANNEL %tuhnairf,1
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
@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"
|
||||
|
||||
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 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:
|
||||
You can use colors in it:
|
||||
|
||||
!search supybot.plugins.ChanTracker.announce
|
||||
!config help supybot.plugins.ChanTracker.announceModes
|
||||
@config channel #myChannel supybot.plugins.ChanTracker.useColorForAnnounces True
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
!config channel #myChannel supybot.plugins.ChanTracker.askOpAboutMode True
|
||||
@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
|
||||
|
||||
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 exception modes (that is exempt e, or invite exempt I) for people banned if 'doActionAgainstAffected' for given channel is True.
|
||||
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`.
|
||||
|
||||
## 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 CTCP to the channel
|
||||
- ctcp: detect sending CTCPs 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 fits your needs, do not use default values. It really depends channel's population and usage ...
|
||||
You should tweak settings to fit your needs, do not use default values. It really depends
|
||||
on the channel's population and usage...
|
||||
|
||||
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:
|
||||
Each of those detections has the same kind of settings:
|
||||
|
||||
- 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
|
||||
- `*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)
|
||||
|
||||
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.
|
||||
The action modes that can be set are:
|
||||
|
||||
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:
|
||||
- `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
|
||||
|
||||
!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
|
||||
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.
|
||||
|
||||
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.
|
||||
**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:
|
||||
|
||||
Example: not flooding: catch a wave of bots which sends the same message from different hosts:
|
||||
@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
|
||||
|
||||
!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
|
||||
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`.
|
||||
|
||||
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 ##
|
||||
|
||||
supybot.reply.error.noCapability: True
|
||||
supybot.reply.whenNotCommand: False
|
||||
supybot.reply.error.detailed: False
|
||||
Maintaining separate bots for the banning/bantracking functions and other factoid, snarfing,
|
||||
or amusement functions is good practice.
|
||||
|
||||
You should also disable help, config, list until needed for registered users on the bot.
|
||||
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`):
|
||||
|
||||
It works with any version of supybot, vanilla, limnoria, etc
|
||||
@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
|
||||
|
||||
## Bugs and Features ##
|
||||
|
||||
Requests can be made via https://github.com/ncoevoet/ChanTracker or in private message to niko on chat.freenode.net.
|
||||
Requests can be made via https://github.com/ncoevoet/ChanTracker
|
||||
or in #chantracker on chat.libera.chat
|
||||
|
11
__init__.py
11
__init__.py
@ -25,7 +25,6 @@
|
||||
# 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.
|
||||
|
||||
###
|
||||
|
||||
"""
|
||||
@ -37,7 +36,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.2"
|
||||
__version__ = "1.5"
|
||||
|
||||
# XXX Replace this with an appropriate author or supybot.Author instance.
|
||||
__author__ = supybot.authors.unknown
|
||||
@ -51,8 +50,12 @@ __url__ = 'https://github.com/ncoevoet/ChanTracker'
|
||||
|
||||
from . import config
|
||||
from . import plugin
|
||||
from imp import reload
|
||||
reload(plugin) # In case we're being reloaded.
|
||||
from . import server
|
||||
from importlib import reload
|
||||
# In case we're being reloaded.
|
||||
reload(config)
|
||||
reload(plugin)
|
||||
reload(server)
|
||||
# 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!
|
||||
|
||||
|
403
config.py
403
config.py
@ -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,320 +42,427 @@ def configure(advanced):
|
||||
|
||||
ChanTracker = conf.registerPlugin('ChanTracker')
|
||||
|
||||
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"""))
|
||||
## global settings
|
||||
|
||||
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, '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"""))
|
||||
|
||||
conf.registerGlobalValue(ChanTracker, 'logsSize',
|
||||
registry.PositiveInteger(60, """number of messages to keep in logs. Note, this is per nick - not per nick per channel"""))
|
||||
registry.PositiveInteger(1, """number of messages to keep in logs. Note, this is per nick - not per nick per channel"""))
|
||||
|
||||
conf.registerGlobalValue(ChanTracker, 'quietCommand',
|
||||
registry.String("CS QUIET $channel $hostmask","""command issued to quiet a user; $channel and $hostmask will be replaced at runtime"""))
|
||||
registry.String("PRIVMSG ChanServ :QUIET $channel $hostmask",
|
||||
"""command issued to quiet a user; $channel and $hostmask will be replaced at runtime"""))
|
||||
|
||||
conf.registerGlobalValue(ChanTracker, 'unquietCommand',
|
||||
registry.String("CS UNQUIET $channel $hostmask","""command issued to unquiet a user $channel and $hostmask will be replaced at runtime"""))
|
||||
registry.String("PRIVMSG ChanServ :UNQUIET $channel $hostmask",
|
||||
"""command issued to unquiet a user; $channel and $hostmask will be replaced at runtime"""))
|
||||
|
||||
conf.registerGlobalValue(ChanTracker, 'announceNagInterval',
|
||||
registry.Integer(300,"""interval between two check about announceNagMode, this setting is global."""))
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'useIpForGateway',
|
||||
registry.Boolean(False, """use *!*@*ip bans instead of *!ident@gateway/* when gateways cloak is found and ends with ip.*"""))
|
||||
registry.Boolean(False, """use *!*@*ip bans instead of *!ident@gateway/* when gateway cloak is found and ends with ip.*"""))
|
||||
|
||||
#now per channel
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'opCommand',
|
||||
registry.String("CS OP $channel $nick", """command used to obtain channel operator mode"""))
|
||||
registry.String("PRIVMSG ChanServ :OP $channel $nick", """command used to obtain channel operator status"""), opSettable=False)
|
||||
|
||||
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"""))
|
||||
|
||||
# per channel settings
|
||||
# related to ban tracking
|
||||
conf.registerChannelValue(ChanTracker, 'ignoreOnAbuse',
|
||||
registry.Boolean(False, """adds abusers on ignore for the punishment period"""))
|
||||
|
||||
## 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's actions; it is highly recommended to set an appropriate operator's channel to receive the various useful messages"""))
|
||||
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"""))
|
||||
|
||||
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','v','o','h','k','n','t','F','i','t','s','n','c','C'],"""announce modes listed to logChannel"""))
|
||||
registry.CommaSeparatedListOfStrings(['b', 'q', 'e', 'I', 'r', 'l', '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 tracker item description edits to logChannel"""))
|
||||
registry.Boolean(True, """announce new expiries on items to logChannel"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'announceMark',
|
||||
registry.Boolean(True,"""announce item expiration settings (marks) to logChannel"""))
|
||||
registry.Boolean(True, """announce new comments on items to logChannel"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'announceInTimeEditAndMark',
|
||||
registry.Boolean(False,"""announce new comments (edits) and expiries (marks) to logChannel when they are created by the do, q, b, e, i commands"""))
|
||||
registry.Boolean(False, """announce new expiries and comments to logChannel when they are created by the do, q, b, e and 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 items comment (edit) to logChannel"""))
|
||||
registry.Boolean(False, """when banning based on a channel protection trigger (such as flood prevention), announce the item expiry to logChannel"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'announceBotMark',
|
||||
registry.Boolean(False,"""when banning based on a channel protection trigger (such as flood prevention), announce the items expiry (mark) to logChannel"""))
|
||||
registry.Boolean(False, """when banning based on a channel protection trigger (such as flood prevention), announce the item comment 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"""))
|
||||
|
||||
# others settings
|
||||
conf.registerChannelValue(ChanTracker, 'announceRepeatPattern',
|
||||
registry.Boolean(True, """announce repeat pattern created to logChannel"""))
|
||||
|
||||
|
||||
## other settings
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'doNothingAboutOwnOpStatus',
|
||||
registry.Boolean(True, """bot will never try to change his own op status"""))
|
||||
registry.Boolean(True, """bot will never try to change its 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 if with caution, if an op bans *!*@*, bot will kick everyone on the channel"""))
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'kickMax',
|
||||
registry.Integer(-1,"""if > 0, disable kick if affected users > kickMax, avoid to cleanup entire channel with ban like *!*@*"""))
|
||||
registry.Integer(-1, """if > 0, disable kick if affected users > kickMax, avoid to clean up entire channel with ban like *!*@*"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'kickMessage',
|
||||
registry.String("You are banned from this channel", """bot kick reason"""))
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'quietMessage',
|
||||
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"""))
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'quietNotice',
|
||||
registry.Boolean(False, """if False, private message is used, if 'quietMessage' is not empty"""))
|
||||
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"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'trackAffected',
|
||||
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"""))
|
||||
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"""))
|
||||
|
||||
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 matchs a permanent ban set in Channel plugin"""))
|
||||
registry.Boolean(True, """when users join the channel, check if user matches 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 $a:account, and 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, or has ip computed"""))
|
||||
|
||||
conf.registerChannelValue(ChanTracker, 'useChanServForQuiets',
|
||||
registry.Boolean(False,"""if bot is not opped, use services for quiet / unquiets"""))
|
||||
registry.Boolean(False, """if bot is not opped, use network services for quiet/unquiets"""))
|
||||
|
||||
# related to channel's protection
|
||||
|
||||
# flood detection settings
|
||||
## 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
|
||||
|
||||
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 of messages's life in flood counter, in seconds"""))
|
||||
registry.PositiveInteger(7, """duration in seconds before messages are removed from counter"""))
|
||||
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 on mode changes database, empty for no comment"""))
|
||||
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
|
||||
|
||||
# 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.Integer(13,"""Duration of messages's life in lowFlood counter, in seconds"""))
|
||||
registry.PositiveInteger(13, """duration in seconds before messages are removed from counter"""))
|
||||
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 on mode changes database, empty for no comment"""))
|
||||
registry.String('low flood detected', """comment added to tracking database, empty for no comment"""))
|
||||
|
||||
|
||||
# repeat detection
|
||||
conf.registerChannelValue(ChanTracker, 'repeatPermit',
|
||||
registry.Integer(-1,"""Number of repeated text allowed, -1 to disable"""))
|
||||
conf.registerChannelValue(ChanTracker, 'repeatLife',
|
||||
registry.PositiveInteger(12,"""Duration of messages's life in repeatPermit counter in seconds"""))
|
||||
conf.registerChannelValue(ChanTracker, 'repeatPercent',
|
||||
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"""))
|
||||
conf.registerChannelValue(ChanTracker, 'repeatPermit',
|
||||
registry.Integer(-1, """number of triggers allowed during repeatLife, -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"""))
|
||||
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"""))
|
||||
|
||||
|
||||
# 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 count"""))
|
||||
registry.PositiveInteger(30, """duration in seconds before messages are removed from counter"""))
|
||||
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 is triggered"""))
|
||||
registry.String('q', """mode used by the bot when cap detection is triggered"""))
|
||||
conf.registerChannelValue(ChanTracker, 'capDuration',
|
||||
registry.PositiveInteger(180,"""punition in seconds"""))
|
||||
registry.PositiveInteger(180, """punishment duration in seconds"""))
|
||||
conf.registerChannelValue(ChanTracker, 'capComment',
|
||||
registry.String('capslock detected',"""comment added on mode changes database, empty for no comment"""))
|
||||
registry.String('capslock detected', """comment added to tracking database, empty for no comment"""))
|
||||
|
||||
|
||||
# hilights
|
||||
|
||||
# hilight
|
||||
conf.registerChannelValue(ChanTracker, 'hilightPermit',
|
||||
registry.Integer(-1,"""Number of nick allowed per message, -1 to disable, note : it doesn't care if it's the same nick"""))
|
||||
registry.Integer(-1, """number of nicks 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 is triggered"""))
|
||||
registry.String('q', """mode used by the bot when hilight detection 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 on mode changes database, empty for no comment"""))
|
||||
registry.String('hilight detected', """comment added to tracking database, empty for no comment"""))
|
||||
|
||||
|
||||
# notices
|
||||
|
||||
# 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 count"""))
|
||||
registry.PositiveInteger(3, """duration in seconds before messages are removed from counter"""))
|
||||
conf.registerChannelValue(ChanTracker, 'noticeMode',
|
||||
registry.String('q',"""mode used by the bot when notice is triggered"""))
|
||||
registry.String('q', """mode used by the bot when notice detection 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 on mode changes database, empty for no comment"""))
|
||||
registry.String('notice detected', """comment added to tracking database, empty for no comment"""))
|
||||
|
||||
|
||||
# ctcps
|
||||
|
||||
# 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 count"""))
|
||||
registry.PositiveInteger(3, """duration in seconds before messages are removed from counter"""))
|
||||
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 on mode changes database, empty for no comment"""))
|
||||
registry.String('ctcp detected', """comment added to tracking database, empty for no comment"""))
|
||||
|
||||
|
||||
# join/part flood
|
||||
|
||||
# channel join/part flood
|
||||
conf.registerChannelValue(ChanTracker, 'cyclePermit',
|
||||
registry.Integer(-1,"""Number of cycles allowed, -1 to disable, count part and quit"""))
|
||||
registry.Integer(-1, """number of cycles allowed, -1 to disable; counts parts and quits"""))
|
||||
conf.registerChannelValue(ChanTracker, 'cycleLife',
|
||||
registry.PositiveInteger(180,"""Duration in seconds before cycles are removed from count"""))
|
||||
registry.PositiveInteger(180, """duration in seconds before cycles are removed from counter"""))
|
||||
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 on mode changes database, empty for no comment"""))
|
||||
registry.String('cycle detected', """comment added to tracking 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"""))
|
||||
registry.String('', """if your ircd supports that, you can forward the user to a specific channel"""))
|
||||
|
||||
|
||||
# massjoin from a host
|
||||
|
||||
# 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 is triggered"""))
|
||||
registry.String('+rq-z $~a', """mode used by the bot when massjoin detection 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.Integer(300,"""Duration in seconds before nick changes are removed from count"""))
|
||||
registry.PositiveInteger(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 is triggered"""))
|
||||
registry.String('q', """mode used by the bot when nick detection 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 on mode changes database, empty for no comment"""))
|
||||
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
|
||||
|
||||
# if you enable this, each time someone trigger other protection that will increase this queue
|
||||
conf.registerChannelValue(ChanTracker, 'badPermit',
|
||||
registry.Integer(-1,"""Number of bad action allowed, -1 to disable, each time bot had to acts on a user, it increase this item"""))
|
||||
registry.Integer(-1, """number of actions allowed, -1 to disable; each time bot had to act on a user, it increases this counter"""))
|
||||
conf.registerChannelValue(ChanTracker, 'badLife',
|
||||
registry.Integer(600,"""Duration in seconds before actions are removed from count"""))
|
||||
registry.PositiveInteger(600, """duration in seconds before actions are removed from counter"""))
|
||||
conf.registerChannelValue(ChanTracker, 'badMode',
|
||||
registry.String('b',"""mode used by the bot when bad is triggered"""))
|
||||
registry.String('b', """mode used by the bot when bad detection 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 on mode changes database, empty for no comment"""))
|
||||
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
|
||||
|
||||
# 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 action allowed, -1 to disable, each time bot flags user as bad, it increase this item"""))
|
||||
registry.Integer(-1, """number of bad actions allowed, -1 to disable; each time bot flags a user as bad, it increases this counter"""))
|
||||
conf.registerChannelValue(ChanTracker, 'attackLife',
|
||||
registry.Integer(600,"""Duration in seconds before actions are removed from count"""))
|
||||
registry.PositiveInteger(600, """duration in seconds before bad actions are removed from counter"""))
|
||||
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 is triggered"""))
|
||||
registry.String('+rq-z $~a', """mode used by the bot when attack detection is triggered"""))
|
||||
conf.registerChannelValue(ChanTracker, 'attackUnMode',
|
||||
registry.String('-rq+z $~a',"""mode used by the bot when attackDuration is finished"""))
|
||||
registry.String('-rq+z $~a', """mode used by the bot when attackDuration is finished"""))
|
||||
|
||||
|
||||
# netsplits
|
||||
|
||||
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"""))
|
||||
|
497
server.py
497
server.py
@ -1,76 +1,68 @@
|
||||
import BaseHTTPServer
|
||||
import os
|
||||
import time
|
||||
import base64
|
||||
import re
|
||||
import os, re, time, base64
|
||||
import supybot.utils as utils
|
||||
import sqlite3
|
||||
import collections
|
||||
import urllib
|
||||
from StringIO import StringIO
|
||||
import http.server, sqlite3
|
||||
|
||||
host = 'http://domain.tld'
|
||||
port = 80
|
||||
standalone = True
|
||||
webpath = '/bantracker'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
filename = '/home/botaccount/data/networkname/ChanTracker.db'
|
||||
channels = [] # empty to allows view of all channels recorded, otherwise restrict the views to channels
|
||||
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 = ''
|
||||
|
||||
# usage python server.py
|
||||
auth = '%s:%s' % (username,password)
|
||||
base64string = base64.b64encode(auth.encode('utf-8')).decode('utf-8')
|
||||
|
||||
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]))
|
||||
def weblink():
|
||||
weblink = host
|
||||
if standalone:
|
||||
weblink += ':%s' % port
|
||||
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:
|
||||
return format('%L', ret)
|
||||
weblink += webpath
|
||||
weblink += '/?hash=%s' % base64string
|
||||
return weblink
|
||||
|
||||
class MyHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
|
||||
server_version= "Ircd-Seven/1.1"
|
||||
def do_GET( self ):
|
||||
self.page( self.path )
|
||||
def htmlEscape(text):
|
||||
return text.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
||||
|
||||
def page (self,query):
|
||||
|
||||
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>' % (' » %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)
|
||||
body = []
|
||||
else:
|
||||
h = '%s/' % webpath
|
||||
if not query:
|
||||
return
|
||||
if query.startswith('/?username='):
|
||||
@ -84,287 +76,260 @@ class MyHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
|
||||
if aa[0] == 'password':
|
||||
p = aa[1]
|
||||
if u and p:
|
||||
raw = base64.encodestring('%s:%s' % (u,p))[:-1]
|
||||
if not raw == base64string:
|
||||
auth = '%s:%s' % (u,p)
|
||||
raw = base64.b64encode(auth.encode('utf-8')).decode('utf-8')
|
||||
if raw != base64string:
|
||||
query = ''
|
||||
else:
|
||||
query = '/?hash=%s' % base64string
|
||||
if not query.startswith('/?hash='):
|
||||
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)
|
||||
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)
|
||||
return
|
||||
if query.startswith('/?hash='):
|
||||
a = query.split('&')[0]
|
||||
a = a.replace('/?hash=','')
|
||||
query = query.replace('%3D','=')
|
||||
query = query.replace('/?hash=%s' % base64string,'/')
|
||||
query = query.replace('/?hash=%s' % base64string,'')
|
||||
query = query.lstrip('&')
|
||||
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')
|
||||
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
|
||||
db = self._getbandb()
|
||||
c = db.cursor()
|
||||
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,))
|
||||
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]
|
||||
(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))))
|
||||
(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&%s">%s</a>' % (bid,h,q,utils.web.urlencode({'oper':oper}),oper),
|
||||
'in <a href="%s%s&channel=%s">%s</a>:' % (h,q,channel.split('#')[1],channel),
|
||||
'+%s <a href="%s%s&%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 = timeElapsed(float(end_at) - float(begin_at))
|
||||
body.append('<p>Original duration : %s</p>\n' % was)
|
||||
was = utils.timeElapsed(float(end_at) - float(begin_at))
|
||||
body.append('<p>Original duration: %s</p>' % 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()))
|
||||
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>%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,))
|
||||
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&%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):
|
||||
users = r
|
||||
body.append('<h3>Logs</h3>\n')
|
||||
for u in users:
|
||||
(full,log) = u
|
||||
body.append('<p>for %s</p>\n' % full)
|
||||
body.append('<h3>Logs</h3>')
|
||||
for (full,log) in r:
|
||||
body.append('<p>for %s</p>' % full)
|
||||
if log != '':
|
||||
body.append('<ul>\n')
|
||||
body.append('<ul>')
|
||||
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,))
|
||||
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.append('<h3>Comments</h3>\n')
|
||||
body.append('<ul>\n')
|
||||
comments = r
|
||||
for com in comments:
|
||||
(oper,at,comment) = com
|
||||
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>\n' % (s,oper,comment))
|
||||
body.append('</ul>\n')
|
||||
elif query.startswith('/&channel='):
|
||||
search = '#'+query.split('/&channel=')[1]
|
||||
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):
|
||||
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]
|
||||
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):
|
||||
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]
|
||||
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):
|
||||
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))
|
||||
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):
|
||||
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),))
|
||||
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):
|
||||
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
|
||||
for ban in r:
|
||||
a[ban[0]] = 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)
|
||||
elif query.startswith('/&search='):
|
||||
search = query.split('/&search=')[1]
|
||||
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
|
||||
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))
|
||||
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):
|
||||
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),))
|
||||
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,))
|
||||
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))
|
||||
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 = []
|
||||
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):
|
||||
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
|
||||
for ban in r:
|
||||
a[ban[0]] = 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')
|
||||
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)
|
||||
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))
|
||||
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.append('<td>%s</td>\n' % s)
|
||||
body.extend([
|
||||
'<tr>',
|
||||
'<td><a href="%s%s&id=%d">%d</a></td>' % (h,q,bid,bid),
|
||||
'<td><a href="%s%s&channel=%s">%s</a></td>' % (h,q,channel.split('#')[1],channel),
|
||||
'<td><a href="%s%s&%s">%s</a></td>' % (h,q,utils.web.urlencode({'oper':oper}),oper),
|
||||
'<td>+%s</td>' % kind,
|
||||
'<td><a href="%s%s&%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>\n' % s)
|
||||
body.append('<td>%s</td>' % s)
|
||||
else:
|
||||
body.append( '<td></td>')
|
||||
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)
|
||||
body.append('<td>%s</td>' % s)
|
||||
else:
|
||||
body.append( '<td></td>\n' )
|
||||
body.append('<td></td>')
|
||||
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))
|
||||
body.append('<td><a href="%s%s&%s">%s</a></td>' % (h,q,utils.web.urlencode({'removed_by':removed_by}),removed_by))
|
||||
else:
|
||||
body.append( '<td></td>\n')
|
||||
body.append('<td></td>')
|
||||
# affected = ''
|
||||
# try:
|
||||
# c.execute("""SELECT full, log FROM nicks WHERE ban_id=?""",(id,))
|
||||
# c.execute("""SELECT full, log FROM nicks WHERE ban_id=?""",(bid,))
|
||||
# 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)
|
||||
# body.append('<td>%s</td>' % affected)
|
||||
body.append('</tr>')
|
||||
body.extend(['</tbody>', '</table></div>'])
|
||||
else:
|
||||
body.append('<p>Nothing found</p>')
|
||||
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,
|
||||
@ -392,10 +357,10 @@ class MyHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
def httpd(handler_class=MyHandler, server_address = ('', port), ):
|
||||
srvr = BaseHTTPServer.HTTPServer(server_address, handler_class)
|
||||
|
||||
def httpd(handler_class=BanTracker, server_address=(servaddr, port)):
|
||||
srvr = http.server.HTTPServer(server_address, handler_class)
|
||||
srvr.serve_forever()
|
||||
|
||||
if __name__ == "__main__":
|
||||
httpd( )
|
||||
|
||||
httpd()
|
||||
|
13
setup.py
Normal file
13
setup.py
Normal file
@ -0,0 +1,13 @@
|
||||
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',
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user