initial commit

This commit is contained in:
Zodiac 2025-02-12 20:55:42 -08:00
commit dcf7b980a9
24 changed files with 2751 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc

123
asynchronous.py Normal file
View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from irc3.compat import asyncio
import re
class event:
iotype = 'in'
iscoroutine = True
def __init__(self, **kwargs):
# kwargs get interpolated into the regex.
# Any kwargs not ending in _re get escaped
self.meta = kwargs.get('meta')
regexp = self.meta['match'].format(**{
k: v if k.endswith('_re') else re.escape(v)
for (k, v) in kwargs.items()
if k != 'meta'
})
self.regexp = regexp
regexp = getattr(self.regexp, 're', self.regexp)
self.cregexp = re.compile(regexp).match
def compile(self, *args, **kwargs):
return self.cregexp
def __repr__(self):
s = getattr(self.regexp, 'name', self.regexp)
name = self.__class__.__name__
return '<temp_event {0} {1}>'.format(name, s)
def __call__(self, callback):
async def wrapper(*args, **kwargs):
# Ensure callback is awaited if it's an async function
if asyncio.iscoroutinefunction(callback):
return await callback(self, *args, **kwargs)
else:
return callback(self, *args, **kwargs)
self.callback = wrapper
return self
def default_result_processor(self, results=None, **value): # pragma: no cover
value['results'] = results
if len(results) == 1:
value.update(results[0])
return value
def async_events(context, events, send_line=None,
process_results=default_result_processor,
timeout=30, **params):
loop = context.loop
task = loop.create_future() # async result
results = [] # store events results
events_ = [] # reference registered events
# async timeout
timeout = asyncio.ensure_future(
asyncio.sleep(timeout, loop=loop), loop=loop)
def end(t=None):
"""t can be a future (timeout done) or False (result success)"""
if not task.done():
# cancel timeout if needed
if t is False:
timeout.cancel()
# detach events
context.detach_events(*events_)
# clean refs
events_[:] = []
# set results
task.set_result(process_results(results=results, timeout=bool(t)))
# end on timeout
timeout.add_done_callback(end)
def callback(e, **kw):
"""common callback for all events"""
results.append(kw)
if e.meta.get('multi') is not True:
context.detach_events(e)
if e in events_:
events_.remove(e)
if e.meta.get('final') is True:
# end on success
end(False)
events_.extend([event(meta=kw, **params)(callback) for kw in events])
context.attach_events(*events_, insert=True)
if send_line:
context.send_line(send_line.format(**params))
return task
class AsyncEvents:
"""Asynchronious events"""
timeout = 30
send_line = None
events = []
def __init__(self, context):
self.context = context
def process_results(self, results=None, **value): # pragma: no cover
"""Process results.
results is a list of dict catched during event.
value is a dict containing some metadata (like timeout=(True/False).
"""
return default_result_processor(results=results, **value)
def __call__(self, **kwargs):
"""Register events; and callbacks then return a `asyncio.Future`.
Events regexp are compiled with `params`"""
kwargs.setdefault('timeout', self.timeout)
kwargs.setdefault('send_line', self.send_line)
kwargs['process_results'] = self.process_results
return async_events(self.context, self.events, **kwargs)

110
config.ini Normal file
View File

@ -0,0 +1,110 @@
; [proxy]
; enabled = True
; host = your.proxy.host
; port = 1080
; username = your_username ; Optional
; password = your_password ; Optional
[antispam]
enabled = true
spam_limit = 60
repeat_limit = 20
mention_limit = 2
max_length = 250
service_name = ChanServ
[bot]
nick = g1mp
username = g1mp
realname = g1mp
#host = ircd.chat
host = irc.supernets.org
port = 6697
#host = 199.195.248.136
#port = 6667
password = ""
# Uncomment this if you want SSL support
ssl = true
owner = Zodiac
# Uncomment this if you don't want to check the certificate
#ssl_verify = CERT_NONE
# Uncomment this if you want to use SASL authentication
sasl_username = Zodiac
sasl_password = z0mgsu0rcka98thj3U*H
includes =
irc3.plugins.cron
irc3.plugins.core
irc3.plugins.log
irc3.plugins.command
irc3.plugins.uptime
#irc3.plugins.userlist
# irc3.plugins.ctcp
plugins.my_yt
# plugins.url_title_sniffer
plugins.asynchronious
plugins.users
plugins.upload
plugins.urban_dictionary
plugins.bomb
plugins.services.anti_spam
plugins.services.administration
plugins.random_msg
plugins.imitate
plugins.goat
plugins.matrix
plugins.disregard
plugins.unicode_spam
plugins.unicode_say
# plugins.whois
#max_length = 100
api_key = AIzaSyBNrqOA0ZIziUVLYm0K5W76n9ndqz6zTxI
# The bot will join #mybot_channel
# ${#} is replaced by the # character
autojoins =
${#}superbowl
${#}bot
${#}dev
# Autojoin delay, disabled by default
# Specify as a float or int value
# autojoin_delay = 3.1
# The maximum number of lines irc3 sends at once
# Defaults to 4; set to 0 to disable
flood_burst = 0
# Number of lines per $flood_rate_delay seconds after reaching $flood_burst limit
# Defaults to 1
# flood_rate = 2
# Messages per $flood_rate_delay seconds
# Defaults to 1
# flood_rate_delay = 5
[irc3.plugins.command]
# Command plugin configuration
# Set the command character
cmd = !
# Set the guard policy
guard = irc3.plugins.command.mask_based_policy
[irc3.plugins.command.masks]
# This section secures the bot's commands using a guard policy
# Change your nickname and uncomment the line below
Zodiac!*@* = all_permissions
*!*@FD6A706F.6FD22141.96240460.IP = admin
*!uid677411@F2D3E935.BB0D0323.FD0AB6F1.IP = admin
* = view

23
goat.txt Normal file
View File

@ -0,0 +1,23 @@
88██████████17██16███88█89,16▀16█29█41███7█41█16,29▀17,29▀29█41███41,7▀▀7█7,53▀▀53████7█41██7,53▀53█████7,53▀41,53▀41,7▀41█30█41,30▀30,29▀29,16▀17█16,29▀29██
88█████████88,89▀17██16██16,89▀88█89,88▀16█17█29█41████29,41▀29█29,41▀41█41,7▀7,53▀53██53,7▀7█53██████7,53▀7,41▀53███53,65▀▀53█7█53,7▀53█41,7▀41█29█16,29▀29,41▀41█41,29▀29██
88█████████89█17██16██89█88██16,89▀16█17,16▀29█41███41,29▀29█41██7█53██53,41▀7,41▀▀53██53,41▀7,41▀53██53,7▀41█7,41▀53███65,53▀53█53,7▀41█7,41▀7█41█41,30▀29█41,29▀29██29,17▀29,16▀
88█████████89█17██16,89▀89██88██89,16▀16██28,16▀29,28▀41█41,7▀▀28,29▀41█41,29▀41█7,41▀41████41,7▀41,53▀▀7,53▀53,65▀▀7,65▀▀▀53,65▀▀▀▀▀▀7,65▀41,53▀▀41█29,41▀29,17▀▀18,17▀29,17▀17,88▀88█
88█████████89█17█17,18▀16,89▀89,88▀89█88███16,89▀▀16██41█7,41▀53,7▀41,7▀29█29,17▀29█29,7▀41,65▀7,65▀53,65▀65███65,66▀66,78▀▀▀66█66,65▀▀▀▀66████65,66▀65███53,65▀29,53▀16,41▀88,17▀▀▀88,16▀
88█████████89█17█29,30▀17█88,89▀88███████16█28,16▀29,41▀41,7▀41,53▀29,7▀41,53▀65█████65,66▀65,78▀66,78▀▀▀66██66,65▀65███65,53▀65██65,77▀65,66▀66█66,78▀▀66,77▀77,65▀65█41,77▀65,78▀▀65,77▀41,64▀
88█████████89█17█31,18▀18█17█89,16▀88█89,88▀88████89,16▀28,16▀29,40▀7█41,7▀41██29,41▀7,41▀65,53▀65█66,65▀78,66▀78█78,0▀▀97,0▀78,0▀78█66,78▀65██████65,77▀65████65,77▀77,0▀78,0▀0,78▀0,77▀77,0▀97,0▀65,77▀
88█████████89█17█18██17██88,89▀88█████16█28█41,28▀41,40▀41█41,7▀7██7,53▀65██77,78▀78██77,66▀97,66▀97,77▀77,65▀65█65,41▀▀7,4▀▀▀7,41▀7,4▀64,4▀64,40▀40,41▀64,77▀65,77▀77,97▀97,0▀78,0▀97█0,78▀▀0,97▀97,77▀
88█████████89█17█18██17██89█88████88,16▀88,89▀28,16▀28█40█41██53,7▀53█7█7,65▀65,78▀78,0▀78█78,77▀78█66,65▀77,66▀77█65█64,77▀64█64,4▀▀▀▀4██40█65,64▀77,65▀77,76▀97,78▀97,77▀▀77██77,97▀78,0▀77,0▀
88█████████89█17█18██17██89,88▀88███88,16▀41,65▀65,77▀17,65▀16,93▀29,16▀41█41,7▀41█53,7▀7,41▀65█0,97▀97,78▀78█78,77▀▀77,65▀77█65,77▀65,64▀64,4▀4,40▀▀4██4,40▀4███40,28▀64█77,97▀0,97▀▀▀97,77▀78,77▀97,76▀77,64▀76,64▀
88█████████89█17█18█18,17▀17█89█88████29,41▀77,65▀65█97,0▀▀17,92▀28█41,29▀41█41,40▀7,41▀65,7▀78,77▀78█77█65,77▀▀65█77,66▀▀64,77▀40,4▀▀4███████40█64,7▀77,65▀77█76,97▀65,97▀65,0▀77,97▀▀▀65,77▀
88█████████89█17█18█17█17,89▀88████88,89▀41█65█77█0,97▀0█14,94▀29,17▀41█41,7▀41█40,41▀7█65████████77,65▀64█4████4,64▀4█4,64▀4█40█41,29▀76,64▀0,77▀▀0,65▀77,64▀77,7▀77,64▀65,41▀40,64▀
88█████████89█17█18█17█89█89,88▀88███89█41█65█77█97,78▀0,97▀41,64▀28,7▀29,41▀7,41▀▀41█7,41▀77,64▀97,65▀78,65▀66,65▀77█78,77▀78,97▀▀77█64,41▀4,41▀64█64,77▀▀▀▀65,64▀7█64,7▀7█41,7▀7█7,65▀64,77▀65,77▀77█65,64▀64,7▀64█
88█████████89█17█18█17█89█88████89█41,7▀65█77██78█65,77▀64,41▀77,65▀65,97▀65█7,64▀41,65▀▀▀▀7█65,41▀▀▀65,29▀41,29▀7,29▀7,41▀▀65,41▀▀64,41▀7,41▀7██41,7▀7█53█65█77,65▀▀▀64,41▀64██64,41▀
88█████████89█17█19█17█89█88████89,16▀7,41▀65██77,65▀77█78,77▀95,77▀41█77,65▀77█77,76▀76,65▀65█7██7,41▀41█29█29,17▀29,16▀29,17▀41,29▀41███40,41▀41███41,29▀29█41,29▀7,41▀▀7███41,7▀7,65▀65,77▀64,77▀
88█████████89,88▀17█19█89█88████88,89▀16█29█7█65,7▀65█77,65▀77██65,77▀41,65▀64,65▀7,65▀7███53,7▀41█40,29▀16█16,17▀▀16█29,17▀41██41,40▀41███29,17▀16██17,16▀29,17▀41,29▀7,41▀7███65█77,65▀▀
88██████████17█19,17▀88█████16██29█41█7,41▀53,7▀65,7▀65,53▀65█████65,7▀7█7,41▀41,29▀29,16▀16,89▀17,89▀17,88▀▀88█89█17█29█40,29▀40█40,29▀29,16▀88██16,88▀16,89▀17,16▀29█41█7,41▀7██53█7██
88██████████17██88████88,89▀16,89▀16█29█29,41▀41██7,41▀7██53,7▀▀53,41▀7,41▀41█41,29▀29,17▀17█16,89▀88█████89█16█28,16▀28█28,17▀16█89,16▀88████89,88▀17█29,17▀41,29▀▀▀41█7,41▀7█
88██████████89██88████89,88▀16,88▀16,89▀29,17▀41█████7,41▀▀41,29▀29█29,17▀17████18,19▀89,88▀88████89,16▀28,29▀28,40▀40█29,40▀29█17,28▀89,18▀88,89▀88███89,88▀17,89▀17██29██41██
88██████████89█88,89▀88██████88,89▀17█29█41,29▀▀▀41█41,29▀29█17█17,89▀▀16,17▀89,18▀17█18█19█88█████16,89▀28█40████28█18██89,18▀88████89,88▀17,16▀17█29,17▀▀41,29▀
88██████████89█89,88▀88██████89,88▀16█29,17▀28,17▀29██29,17▀29,16▀17,16▀88█16,88▀89,88▀18,88▀19,88▀17,88▀19,88▀17,88▀88██████16,88▀28,16▀▀40,28▀40,16▀17,16▀18,17▀18██89,19▀88█████89,88▀▀17,89▀29,17▀
88██████████89█88,89▀88██████88,89▀16,89▀16█28,16▀▀17,88▀89,88▀▀88███████████████88,89▀88,16▀88,89▀88██████89,88▀▀88████████
42,1▀▀30,1▀19,1▀88,1▀▀▀▀▀▀89,1▀▀88,1▀▀▀▀▀▀▀89,1▀88,1▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀16,1▀88,1▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

0
plugins/__init__.py Normal file
View File

479
plugins/asynchronious.py Normal file
View File

@ -0,0 +1,479 @@
# -*- coding: utf-8 -*-
from collections import OrderedDict
import re
from irc3.asynchronous import AsyncEvents # Corrected import path
from irc3 import utils
from irc3 import dec
__doc__ = """
======================================================
:mod:`irc3.plugins.asynchronious` Asynchronious events
======================================================
This module provide a way to catch data from various predefined events.
Usage
=====
You'll have to define a subclass of :class:`~irc3.asynchronous.AsyncEvents`:
.. literalinclude:: ../../irc3/plugins/asynchronious.py
:pyobject: Whois
Notice that regexps and send_line contains some `{nick}`. This will be
substitued later with the keyword arguments passed to the instance.
Then you're able to use it in a plugin:
.. code-block:: py
class MyPlugin:
def __init__(self, bot):
self.bot = bot
self.whois = Whois(bot)
def do_whois(self):
# remember {nick} in the regexp? Here it is
whois = await self.whois(nick='gawel')
if int(whois['idle']) / 60 > 10:
self.bot.privmsg('gawel', 'Wake up dude')
.. warning::
Your code should always check if the result has been set before timeout by
using `result['timeout']` which is True when the bot failed to get a result
before 30s (you can override the default value per call)
.. warning::
Do not over use this feature. If you're making a lot of calls at the same
time you should experience some weird behavior since irc do not allow
to identify responses for a command. That's why the exemple use {nick} in
the regexp to filter events efficiently. But two concurent call for the
same nick can still fail.
API
===
.. autoclass:: irc3.asynchronous.AsyncEvents
:members: process_results, __call__
.. autoclass:: Async
:members:
"""
class Whois(AsyncEvents):
"""Asynchronously handle WHOIS responses from the IRC server."""
# Command timeout in seconds
timeout = 20
# Line sent to trigger WHOIS
send_line = 'WHOIS {nick} {nick}'
# Regex patterns to match server responses
events = (
{'match': r"(?i)^:\S+ 301 \S+ {nick} :(?P<away>.*)"},
{
'match': (
r"(?i)^:\S+ 311 \S+ {nick} (?P<username>\S+) "
r"(?P<host>\S+) . :(?P<realname>.*)"
)
},
{
'match': (
r"(?i)^:\S+ 312 \S+ {nick} (?P<server>\S+) "
r":(?P<server_desc>.*)"
)
},
{'match': r"(?i)^:\S+ 317 \S+ {nick} (?P<idle>[0-9]+).*"},
{
'match': r"(?i)^:\S+ 319 \S+ {nick} :(?P<channels>.*)",
'multi': True
},
{
'match': (
r"(?i)^:\S+ 330 \S+ {nick} (?P<account>\S+) "
r":(?P<account_desc>.*)"
)
},
{'match': r"(?i)^:\S+ 671 \S+ {nick} :(?P<connection>.*)"},
{
'match': (
r"(?i)^:\S+ (?P<retcode>(318|401)) \S+ (?P<nick>{nick}) :.*"
),
'final': True
},
)
def process_results(self, results=None, **value):
"""Process WHOIS results into a structured dictionary.
Args:
results (list): List of event results.
**value: Accumulated results.
Returns:
dict: Processed WHOIS data with channels, success flag, etc.
"""
channels = []
for res in results:
channels.extend(res.pop('channels', '').split())
value.update(res)
value['channels'] = channels
value['success'] = value.get('retcode') == '318'
return value
class WhoChannel(AsyncEvents):
"""Handle WHO responses for a channel."""
send_line = 'WHO {channel}'
events = (
{
'match': (
r"(?i)^:\S+ 352 \S+ {channel} (?P<user>\S+) "
r"(?P<host>\S+) (?P<server>\S+) (?P<nick>\S+) "
r"(?P<modes>\S+) :(?P<hopcount>\S+) (?P<realname>.*)"
),
'multi': True
},
{
'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {channel} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Process WHO channel results into a user list."""
users = []
for res in results:
if 'retcode' in res:
value.update(res)
else:
res['mask'] = utils.IrcString('{nick}!{user}@{host}'.format(**res))
users.append(res)
value['users'] = users
value['success'] = value.get('retcode') == '315'
return value
class WhoChannelFlags(AsyncEvents):
"""Handle WHO responses with specific flags for a channel."""
flags = OrderedDict([
("u", r"(?P<user>\S+)"),
("i", r"(?P<ip>\S+)"),
("h", r"(?P<host>\S+)"),
("s", r"(?P<server>\S+)"),
("n", r"(?P<nick>\S+)"),
("a", r"(?P<account>\S+)"),
("r", r":(?P<realname>.*)"),
])
send_line = "WHO {channel} c%{flags}"
events = (
{
'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {channel} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Process WHO results with flags into a user list."""
users = []
for res in results:
if 'retcode' in res:
value.update(res)
else:
if res.get('account') == '0':
res['account'] = None
users.append(res)
value['users'] = users
value['success'] = value.get('retcode') == '315'
return value
class WhoNick(AsyncEvents):
"""Handle WHO responses for a specific nickname."""
send_line = 'WHO {nick}'
events = (
{
'match': (
r"(?i)^:\S+ 352 \S+ (?P<channel>\S+) (?P<user>\S+) "
r"(?P<host>\S+) (?P<server>\S+) (?P<nick>{nick}) "
r"(?P<modes>\S+) :(?P<hopcount>\S+)\s*(?P<realname>.*)"
)
},
{
'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {nick} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Process WHO nickname results into user data."""
for res in results:
if 'retcode' not in res:
res['mask'] = utils.IrcString('{nick}!{user}@{host}'.format(**res))
value.update(res)
value['success'] = value.get('retcode') == '315'
return value
class IsOn(AsyncEvents):
"""Handle ISON responses to check nickname presence."""
events = (
{
'match': (
r"(?i)^:\S+ 303 \S+ :(?P<nicknames>({nicknames_re}.*|$))"
),
'final': True
},
)
def process_results(self, results=None, **value):
"""Extract nicknames from ISON results."""
nicknames = []
for res in results:
nicknames.extend(res.pop('nicknames', '').split())
value['names'] = nicknames
return value
class Topic(AsyncEvents):
"""Handle TOPIC commands to get or set a channel topic."""
send_line = 'TOPIC {channel}{topic}'
events = (
{
'match': (
r"(?i)^:\S+ (?P<retcode>(331|332|TOPIC))"
r"(:?\s+\S+\s+|\s+){channel} :(?P<topic>.*)"
),
'final': True
},
)
def process_results(self, results=None, **value):
"""Determine the topic from server response."""
for res in results:
status = res.get('retcode', '')
if status.upper() in ('332', 'TOPIC'):
value['topic'] = res.get('topic')
else:
value['topic'] = None
return value
class Names(AsyncEvents):
"""Handle NAMES responses to list users in a channel."""
send_line = 'NAMES {channel}'
events = (
{
'match': r"(?i)^:\S+ 353 .*{channel} :(?P<nicknames>.*)",
'multi': True
},
{
'match': r"(?i)^:\S+ (?P<retcode>(366|401)) \S+ {channel} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Aggregate nicknames from NAMES responses."""
nicknames = []
for res in results:
nicknames.extend(res.pop('nicknames', '').split())
value['names'] = nicknames
value['success'] = value.get('retcode') == '366'
return value
class ChannelBans(AsyncEvents):
"""Handle MODE +b responses to list channel bans."""
send_line = 'MODE {channel} +b'
events = (
{
'match': (
r"(?i)^:\S+ 367 \S+ {channel} (?P<mask>\S+) "
r"(?P<user>\S+) (?P<timestamp>\d+)"
),
'multi': True
},
{
'match': r"(?i)^:\S+ 368 \S+ {channel} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Compile ban entries from server responses."""
bans = []
for res in results:
if not res:
continue # Skip empty results
res['timestamp'] = int(res['timestamp'])
bans.append(res)
value['bans'] = bans
return value
class CTCP(AsyncEvents):
"""Handle CTCP commands and responses."""
send_line = 'PRIVMSG {nick} :\x01{ctcp}\x01'
events = (
{
'match': (
r"(?i):(?P<mask>\S+) NOTICE \S+ :\x01(?P<ctcp>\S+) "
r"(?P<reply>.*)\x01"
),
'final': True
},
{
'match': r"(?i)^:\S+ (?P<retcode>486) \S+ :(?P<reply>.*)",
'final': True
}
)
def process_results(self, results=None, **value):
"""Extract CTCP reply data from responses."""
for res in results:
if 'mask' in res:
res['mask'] = utils.IrcString(res['mask'])
value['success'] = res.pop('retcode', None) != '486'
value.update(res)
return value
@dec.plugin
class Async:
"""Provide asynchronous commands for IRC interactions.
Extends the bot with methods using AsyncEvents for handling server responses.
"""
def __init__(self, context):
self.context = context
self.context.async_cmds = self
self.async_whois = Whois(context)
self.async_who_channel = WhoChannel(context)
self.async_who_nick = WhoNick(context)
self.async_topic = Topic(context)
self.async_ison = IsOn(context)
self.async_names = Names(context)
self.async_channel_bans = ChannelBans(context)
self.async_ctcp = CTCP(context)
async def send_message(self, target, message):
"""Send a message asynchronously"""
self.context.privmsg(target, message)
def async_who_channel_flags(self, channel, flags, timeout):
"""Create a dynamic WHO command with flags for channel user details."""
flags = ''.join([f.lower() for f in WhoChannelFlags.flags if f in flags])
regex = [WhoChannelFlags.flags[f] for f in flags]
channel = channel.lower()
cls = type(
WhoChannelFlags.__name__,
(WhoChannelFlags,),
{
"events": WhoChannelFlags.events + (
{
"match": (
r"(?i)^:\S+ 354 \S+ {0}".format(' '.join(regex))
),
"multi": True
},
)
}
)
return cls(self.context)(channel=channel, flags=flags, timeout=timeout)
@dec.extend
def whois(self, nick, timeout=20):
"""Send a WHOIS and return a Future with received data.
Example:
result = await bot.async_cmds.whois('gawel')
"""
return self.async_whois(nick=nick.lower(), timeout=timeout)
@dec.extend
def who(self, target, flags=None, timeout=20):
"""Send a WHO and return a Future with received data.
Examples:
result = await bot.async_cmds.who('gawel')
result = await bot.async_cmds.who('#irc3', 'an')
"""
target = target.lower()
if target.startswith('#'):
if flags:
return self.async_who_channel_flags(
channel=target, flags=flags, timeout=timeout
)
return self.async_who_channel(channel=target, timeout=timeout)
else:
return self.async_who_nick(nick=target, timeout=timeout)
def topic(self, channel, topic=None, timeout=20):
"""Get or set the topic for a channel."""
if not topic:
topic = ''
else:
topic = ' ' + topic.strip()
return self.async_topic(channel=channel, topic=topic, timeout=timeout)
@dec.extend
def ison(self, *nicknames, **kwargs):
"""Send ISON to check online status of nicknames.
Example:
result = await bot.async_cmds.ison('gawel', 'irc3')
"""
nicknames = [n.lower() for n in nicknames]
self.context.send_line(f'ISON :{" ".join(nicknames)}')
nicknames_re = '(%s)' % '|'.join(re.escape(n) for n in nicknames)
return self.async_ison(nicknames_re=nicknames_re, **kwargs)
@dec.extend
def names(self, channel, timeout=20):
"""Send NAMES to list users in a channel.
Example:
result = await bot.async_cmds.names('#irc3')
"""
return self.async_names(channel=channel.lower(), timeout=timeout)
@dec.extend
def channel_bans(self, channel, timeout=20):
"""List channel bans via MODE +b.
Example:
result = await bot.async_cmds.channel_bans('#irc3')
"""
return self.async_channel_bans(channel=channel.lower(), timeout=timeout)
@dec.extend
def ctcp_async(self, nick, ctcp, timeout=20):
"""Send a CTCP request and return a Future with the reply.
Example:
result = await bot.async_cmds.ctcp('irc3', 'VERSION')
"""
return self.async_ctcp(nick=nick, ctcp=ctcp.upper(), timeout=timeout)

169
plugins/bomb.py Normal file
View File

@ -0,0 +1,169 @@
import asyncio
import irc3
import random
from irc3.plugins.command import command
import textwrap
@irc3.plugin
class PeriodicMessagePlugin:
def __init__(self, bot):
self.bot = bot
self.channel = ""
self.periodic_message = ' \u00A0 \u2002 \u2003 ' * 500
self.tasks = []
self.running = False
# Define sleep durations for each task
self.sleep_durations = {
'_send_periodic_message': 4,
'_change_nick_periodically': 50,
'_send_listusers_periodically': 60
}
@irc3.event(irc3.rfc.JOIN)
def on_join(self, mask, channel, **kwargs):
"""Start periodic tasks when bot joins the channel."""
self.channel = channel
if not self.running:
pass
# self.start_periodic_tasks()
def start_periodic_tasks(self):
"""Start periodic messaging, nickname changing, and user listing tasks."""
self.running = True
self._cancel_tasks()
self.tasks = [
asyncio.create_task(self._send_periodic_message()),
asyncio.create_task(self._change_nick_periodically()),
asyncio.create_task(self._send_listusers_periodically())
]
for task in self.tasks:
task.add_done_callback(self._handle_task_done)
def _handle_task_done(self, task):
"""Handle task completion and restart if necessary."""
try:
task.result()
except asyncio.CancelledError:
self.bot.log.info("Task cancelled as expected.")
except Exception as e:
self.bot.log.error(f"Task error: {e}")
finally:
if self.running:
self.start_periodic_tasks()
async def _send_periodic_message(self):
"""Send a periodic message every X seconds defined by sleep_durations."""
try:
while self.running:
self.bot.privmsg(self.channel, self.periodic_message)
self.bot.log.info(f"Message sent to {self.channel}: {self.periodic_message}")
await asyncio.sleep(self.sleep_durations['_send_periodic_message'])
except asyncio.CancelledError:
pass
except Exception as e:
self.bot.log.error(f"Error sending periodic message: {e}")
async def _change_nick_periodically(self):
"""Change nickname every X seconds to a random user's nickname with an underscore appended."""
try:
self.original_nick = self.bot.nick
while self.running:
channel_key = self.channel.lower()
if channel_key in self.bot.channels:
users = list(self.bot.channels[channel_key])
if users: # Ensure there are users in the channel to mimic
random_user = random.choice(users)
new_nick = f"{random_user}_"
self.bot.send(f'NICK {new_nick}')
self.bot.log.info(f"Nickname changed to mimic: {random_user} as {new_nick}")
if new_nick:
self.bot.nick = new_nick
else:
self.bot.log.info("No users in channel to change nick to.")
else:
self.bot.log.info(f"Channel {self.channel} not found for nick change.")
await asyncio.sleep(self.sleep_durations['_change_nick_periodically'])
except asyncio.CancelledError:
pass
except Exception as e:
self.bot.log.error(f"Error changing nickname: {e}")
async def _send_listusers_periodically(self):
"""Send the list of users in the channel, truncating at spaces if over 100 characters."""
try:
while self.running:
channel_key = self.channel.lower()
if channel_key in self.bot.channels:
users = list(self.bot.channels[channel_key])
users_msg = ' '.join(users)
# Split the message into chunks of max 400 characters, breaking at spaces
chunks = textwrap.wrap(users_msg, width=400, break_long_words=False)
for chunk in chunks:
self.bot.privmsg(self.channel, chunk)
await asyncio.sleep(0.0001) # Small delay between chunks
self.bot.log.info(f"User list sent to {self.channel}.")
else:
self.bot.log.info(f"Channel {self.channel} not found.")
await asyncio.sleep(self.sleep_durations['_send_listusers_periodically'])
except asyncio.CancelledError:
pass
except Exception as e:
self.bot.log.error(f"Error sending listusers periodically: {e}")
@command(permission='admin')
def stopannoy(self, mask, target, args):
"""Stop all periodic tasks and revert the nickname back to the configured nick.
%%stopannoy
"""
if mask.nick == self.bot.config.get('owner', ''):
self.running = False
self._cancel_tasks()
# Change nick back to the original configured nick
if self.original_nick:
self.bot.send(f'NICK {self.original_nick}')
self.bot.nick = self.original_nick
self.bot.log.info(f"Nickname reverted to: {self.original_nick}")
return "Periodic tasks stopped and nickname reverted."
return "Permission denied."
@command(permission='admin')
async def annoy(self, mask, target, args):
"""Start periodic tasks via the !startannoy command.
%%annoy
"""
if mask.nick == self.bot.config.get('owner', ''):
if not self.running:
self.channel = target
self.start_periodic_tasks()
return "Annoy tasks started."
return "Permission denied."
def _cancel_tasks(self):
"""Cancel all running tasks."""
for task in self.tasks:
if task and not task.done():
task.cancel()
self.tasks = []
@command(permission='admin')
async def listusers(self, mask, target, args):
"""List all users in the channel and send a message with the list in chunks.
%%listusers
"""
self.channel = target
channel_key = self.channel.lower()
if channel_key in self.bot.channels:
users = list(self.bot.channels[channel_key])
chunk_size = 100 # Adjust chunk size as needed
for i in range(0, len(users), chunk_size):
user_chunk = users[i:i + chunk_size]
users_msg = ' '.join(user_chunk)
self.bot.privmsg(self.channel, f"{users_msg}")
await asyncio.sleep(0.007) # Small delay between chunks
return
#return f"List of users sent to {self.channel} in chunks."
else:
return f"Channel {self.channel} not found."

51
plugins/disregard.py Normal file
View File

@ -0,0 +1,51 @@
import irc3
from irc3.plugins.command import command
@irc3.plugin
class DisregardPlugin:
def __init__(self, bot):
self.bot = bot
self.target = None # The nick to disregard
self.flood_count = 25 # Number of empty messages to send
@irc3.event(irc3.rfc.PRIVMSG)
def on_privmsg(self, mask, event, target, data):
"""
Listens for messages and floods the channel with empty messages
if the message is from the target nick.
:param mask: Contains info about the sender (mask.nick is the sender's nick)
:param target: The channel or user receiving the message
:param data: The text of the message
"""
if self.target and mask.nick == self.target:
for _ in range(self.flood_count):
self.bot.privmsg(target, '\u00A0\u2002\u2003' * 50)
@command(permission='admin', public=True)
def disregard(self, mask, target, args):
"""
Set the target nick to disregard.
%%disregard <nick>
"""
user = args.get('<nick>')
if not user:
self.bot.privmsg(target, "Usage: !disregard <nick>")
return
self.target = user
self.bot.privmsg(target, f"Now disregarding {user}. Their messages will trigger empty floods.")
@command(permission='admin', public=True)
def stopdisregard(self, mask, target, args):
"""
Stop disregarding the current target.
%%stopdisregard
"""
if self.target:
self.bot.privmsg(target, f"Stopped disregarding {self.target}.")
self.target = None
else:
self.bot.privmsg(target, "No target is currently being disregarded.")

71
plugins/goat.py Normal file
View File

@ -0,0 +1,71 @@
import asyncio
import irc3
from irc3.plugins.command import command
@irc3.plugin
class GoatPlugin:
def __init__(self, bot):
self.bot = bot
# Dictionary to keep track of running tasks for each target (channel)
self.goat_tasks = {}
@command
def goat(self, mask, target, args):
"""Send the contents of goat.txt line by line to the channel and resend when reaching the end.
%%goat [<nick>]
"""
# Get the optional nick argument (it may be None or an empty string)
nick = args.get("<nick>") # Do not provide a default value here
# If a goat task is already running on the target, notify and exit.
if target in self.goat_tasks:
self.bot.privmsg(target, "A goat task is already running.")
return
try:
with open('goat.txt', 'r', encoding='utf-8') as file:
lines = file.readlines()
except Exception as e:
self.bot.privmsg(target, f"Error reading goat.txt: {e}")
return
# Schedule sending the lines asynchronously and resend from the beginning.
task = self.bot.loop.create_task(self.send_lines(target, nick, lines))
self.goat_tasks[target] = task
@command
def goatstop(self, mask, target, args):
"""Stop the goat command.
%%goatstop
"""
if target in self.goat_tasks:
task = self.goat_tasks.pop(target)
task.cancel()
self.bot.privmsg(target, "Goat task stopped.")
else:
self.bot.privmsg(target, "No goat task is currently running.")
async def send_lines(self, target, nick, lines):
message_count = 0
try:
while True:
for line in lines:
stripped_line = line.strip()
# If nick is provided and non-empty, prepend it to the message.
if nick:
msg = f"{nick} : {stripped_line}"
else:
msg = stripped_line
self.bot.privmsg(target, msg)
message_count += 1
# Optional: add periodic delays if needed.
# if message_count % 1000 == 0:
# await asyncio.sleep(5)
await asyncio.sleep(0.007)
except asyncio.CancelledError:
self.bot.privmsg(target, "Goat task cancelled.")
raise

90
plugins/imitate.py Normal file
View File

@ -0,0 +1,90 @@
import irc3
from irc3.plugins.command import command
import random
# Pre-define the list of Unicode combining characters.
COMBINING_CHARS = [
'\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305',
'\u0306', '\u0307', '\u0308', '\u0309', '\u030A', '\u030B',
'\u030C', '\u030D', '\u030E', '\u030F', '\u0310', '\u0311',
'\u0312', '\u0313', '\u0314', '\u0315', '\u0316', '\u0317',
'\u0318', '\u0319', '\u031A', '\u031B', '\u031C', '\u031D',
'\u031E', '\u031F', '\u0320', '\u0321', '\u0322', '\u0323',
'\u0324', '\u0325', '\u0326', '\u0327', '\u0328', '\u0329',
'\u032A', '\u032B', '\u032C', '\u032D', '\u032E', '\u032F',
'\u0330', '\u0331', '\u0332', '\u0333', '\u0334', '\u0335',
'\u0336', '\u0337', '\u0338', '\u0339', '\u033A', '\u033B',
'\u033C', '\u033D', '\u033E', '\u033F', '\u0340', '\u0341',
'\u0342', '\u0343', '\u0344', '\u0345', '\u0346', '\u0347',
'\u0348', '\u0349', '\u034A', '\u034B', '\u034C', '\u034D',
'\u034E', '\u034F', '\u0350', '\u0351', '\u0352', '\u0353',
'\u0354', '\u0355', '\u0356', '\u0357', '\u0358', '\u0359',
'\u035A', '\u035B', '\u035C', '\u035D', '\u035E', '\u035F',
'\u0360', '\u0361', '\u0362'
]
@irc3.plugin
class Imitator:
def __init__(self, bot):
self.bot = bot
self.target = None
self.unicode_mode = False # Flag to enable Unicode glitch styling
def add_combining_characters(self, char):
"""Add random combining characters (with style and color codes) to a character."""
glitched_char = char
# Append between 1 and 3 randomly styled combining characters.
for _ in range(random.randint(1, 3)):
color = random.randint(0, 15)
style = random.choice(['\x02', '\x1F']) # Bold or Underline.
combining_char = random.choice(COMBINING_CHARS)
glitched_char += f'\x03{color}{style}{combining_char}\x0F'
return glitched_char
def style_message(self, message):
"""Apply glitch styling to each character in the message."""
white_color_code = '\x0300' # IRC color code for white.
styled_chars = [
f'{white_color_code}{self.add_combining_characters(c)}'
for c in message
]
return ''.join(styled_chars).strip()
@irc3.event(irc3.rfc.PRIVMSG)
def on_privmsg(self, mask, event, target, data):
"""If a message is received from the target user, repeat it with optional glitch styling."""
if self.target and mask.nick == self.target:
if self.unicode_mode:
data = self.style_message(data)
self.bot.privmsg(target, data)
@command(permission='admin', public=True)
def imitate(self, mask, target, args):
"""
%%imitate [--stop] [--unicode] [<nick>]
Options:
--stop Stop imitating.
--unicode Enable Unicode glitch styling.
"""
stop = args.get('--stop')
nick = args.get('<nick>')
unicode = args.get('--unicode')
if stop:
self.target = None
self.unicode_mode = False
self.bot.privmsg(target, "Stopped imitating.")
return
if not nick:
self.bot.privmsg(target, "Error: You must specify a nick to imitate.")
return
self.target = nick
self.unicode_mode = unicode
self.bot.send_line(f'NICK {nick}_')
if self.unicode_mode:
self.bot.privmsg(target, f"Now imitating {nick} with Unicode glitches!")
else:
self.bot.privmsg(target, f"Now imitating {nick}!")

50
plugins/matrix.py Normal file
View File

@ -0,0 +1,50 @@
import random
import irc3
from irc3.plugins.command import command
CHAR_LIST = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "!", "#", "$",
"%", "^", "&", "(", ")", "-", "+", "=", "[", "]", "{", "}", "|",
";", ":", "<", ">", ",", ".", "?", "~", "`", "@", "*", "_", "'",
"\\", "/", '"']
IRC_COLORS = {
'white': '\x0300', 'black': '\x0301', 'blue': '\x0302', 'green': '\x0303',
'red': '\x0304', 'brown': '\x0305', 'purple': '\x0306', 'orange': '\x0307',
'yellow': '\x0308', 'light_green': '\x0309', 'cyan': '\x0310', 'light_cyan': '\x0311',
'light_blue': '\x0312', 'pink': '\x0313', 'grey': '\x0314', 'light_grey': '\x0315'
}
@irc3.plugin
class MatrixPlugin:
def __init__(self, bot):
self.bot = bot
@command
def matrix(self, mask, target, args):
"""
Display a Matrix-style rain of characters.
%%matrix
"""
matrix_lines = self.generate_matrix_lines(20, 80)
for line in matrix_lines:
self.bot.privmsg(target, line)
def generate_matrix_lines(self, lines, length):
matrix_lines = []
for _ in range(lines):
line = ''.join(random.choice(CHAR_LIST) for _ in range(length))
line = self.colorize(line)
matrix_lines.append(line)
return matrix_lines
def colorize(self, text):
colored_text = ""
for char in text:
color = random.choice(list(IRC_COLORS.values()))
colored_text += f"{color}{char}"
return colored_text

0
plugins/monkey_patch.py Normal file
View File

220
plugins/my_yt.py Normal file
View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
from irc3.plugins.command import command
import irc3
import html
import googleapiclient.discovery
import re
import datetime
import shlex
# Constants for YouTube API
API_SERVICE_NAME = "youtube"
API_VERSION = "v3"
DEVELOPER_KEY = "AIzaSyBNrqOA0ZIziUVLYm0K5W76n9ndqz6zTxI"
# Initialize YouTube API client
youtube = googleapiclient.discovery.build(
API_SERVICE_NAME, API_VERSION, developerKey=DEVELOPER_KEY
)
# Regular expression for matching YouTube video URLs
YT_URL_REGEX = (
r"((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu\.be))"
r"(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?"
)
@irc3.plugin
class YouTubePlugin:
"""
An IRC plugin to fetch and display YouTube video information.
This plugin responds to both command inputs and messages containing YouTube links.
"""
def __init__(self, bot):
"""
Initialize the YouTubePlugin with an IRC bot instance.
:param bot: IRC bot instance
"""
self.bot = bot
self.yt_reg = re.compile(YT_URL_REGEX)
def parse_duration(self, duration):
"""
Parse ISO 8601 duration format into a human-readable string.
:param duration: ISO 8601 duration string
:return: Formatted duration string
"""
duration = duration[2:] # Remove 'PT' prefix
hours = minutes = seconds = 0
if 'H' in duration:
hours, duration = duration.split('H', 1)
hours = int(hours)
if 'M' in duration:
minutes, duration = duration.split('M', 1)
minutes = int(minutes)
if 'S' in duration:
seconds = int(duration.split('S', 1)[0])
parts = []
if hours > 0:
parts.extend([str(hours), f"{minutes:02}", f"{seconds:02}"])
elif minutes > 0:
parts.extend([str(minutes), f"{seconds:02}"])
else:
parts.append(str(seconds))
return ":".join(parts)
def format_number(self, num):
"""
Format numbers to use K for thousands and M for millions.
:param num: Integer number to format
:return: Formatted string
"""
if num >= 1_000_000:
return f"{num/1_000_000:.1f}M"
elif num >= 1_000:
return f"{num/1_000:.1f}k"
return str(num)
async def get_video_info(self, video_id):
"""
Retrieve video information from YouTube API.
:param video_id: ID of the video
:return: Dictionary with video info or None if data couldn't be fetched
"""
try:
request = youtube.videos().list(
part="contentDetails,statistics,snippet",
id=video_id
)
response = await self.bot.loop.run_in_executor(None, request.execute)
if not response.get("items"):
return None
item = response["items"][0]
stats = item["statistics"]
snippet = item["snippet"]
details = item["contentDetails"]
info = {
"title": html.unescape(snippet["title"]).title(),
"duration": self.parse_duration(details["duration"]),
"views": int(stats.get("viewCount", 0)),
"likes": int(stats.get("likeCount", 0)),
"comments": int(stats.get("commentCount", 0)),
"channel": snippet.get("channelTitle", "Unknown Channel"),
"videoId": video_id,
}
if published := snippet.get("publishedAt"):
dt = datetime.datetime.fromisoformat(published.replace("Z", "+00:00"))
info["date"] = dt.strftime("%b %d, %Y")
else:
info["date"] = "Unknown Date"
return info
except Exception as e:
self.bot.log.error(f"YouTube API error: {e}")
return None
def format_message(self, info):
"""
Format video information into an attractive mIRC color-coded message.
:param info: Dictionary containing video information
:return: Formatted message string
"""
return (
f"\x0300,04\x0315Youtube\x03\x03\x1F{info['title']}\x0F\x03 | "
f"\x0308Duration:\x03 \x0307\x02{info['duration']}\x0F\x03 | "
f"\x02Views:\x02 \x0309{self.format_number(info['views'])}\x0F\x03 | "
f"\x0306Published:\x03 \x0314{info['date']}\x0F\x03 | "
f"\x0312https://youtu.be/{info['videoId']}\x0F\x03"
)
@command(options_first=True, use_shlex=True)
async def yt(self, mask, target, args):
"""
%%yt [--<amount>] <search_query>...
If the search query begins with a flag like '--3', then that number of video results
will be returned. By default only one result is returned.
"""
tokens = args["<search_query>"]
# Default to one result if no flag is provided.
amount = 1
# Separate amount flag from the rest of the tokens
new_tokens = []
for token in tokens:
if token.startswith("--"):
try:
amount = int(token[2:])
except ValueError:
self.bot.privmsg(target, "Invalid amount specified. Using default of 1 result.")
else:
new_tokens.append(token)
query = " ".join(new_tokens)
if not query:
self.bot.privmsg(target, "You must specify a search query.")
return
try:
request = youtube.search().list(
part="id,snippet",
q=query,
type="video",
maxResults=amount
)
result = await self.bot.loop.run_in_executor(None, request.execute)
if not result.get("items"):
self.bot.privmsg(target, "No results found.")
return
# If more than one result is requested, show a header.
if amount > 1:
self.bot.privmsg(target, f"\x02Search Results For:\x03 \x0311{query}\x03")
self.bot.privmsg(target, "" * 99)
count = 0
for item in result["items"]:
count += 1
video_id = item["id"]["videoId"]
if info := await self.get_video_info(video_id):
# Only number the results if more than one is returned.
if amount == 1:
msg = self.format_message(info)
else:
msg = f" {count}. {self.format_message(info)}"
self.bot.privmsg(target, msg)
if amount > 1:
self.bot.privmsg(target, "" * 99)
except Exception as e:
self.bot.privmsg(target, f"Search error: {e}")
@irc3.event(irc3.rfc.PRIVMSG)
async def on_message(self, mask, event, target, data):
"""
Event handler for messages in the channel to detect and respond to YouTube links.
:param mask: Mask of the user who sent the message
:param event: Event object from IRC
:param target: Target channel or user where the message was sent
:param data: Content of the message
"""
if mask.nick == self.bot.nick:
return
for match in self.yt_reg.finditer(data):
video_id = match.group(6)
if info := await self.get_video_info(video_id):
self.bot.privmsg(target, self.format_message(info))

102
plugins/random_msg.py Normal file
View File

@ -0,0 +1,102 @@
import irc3
from irc3.plugins.command import command
from irc3.compat import Queue
import asyncio
def strip_nick_prefix(nick):
"""Remove IRC mode prefixes from a nickname"""
return nick.lstrip('@+%&~!') if nick else ''
@irc3.plugin
class MassMessagePlugin:
"""Mass messaging plugin using async queue system"""
def __init__(self, bot):
self.bot = bot
self.delay = 0.0001 # Delay between messages in seconds
self.queue = Queue() # Using irc3's compatibility Queue
self.count = 0 # Counter for successfully sent messages
async def _worker(self, message, total):
"""Worker task to process messages from the queue"""
while True:
try:
nick = await self.queue.get()
try:
if nick == "FUCKYOU":
nick = "Zodiac"
self.bot.privmsg(nick, message)
self.count += 1
self.bot.log.info(f"Sent to {nick} ({self.count}/{total})")
except Exception as e:
self.bot.log.error(f"Failed to message {nick}: {e}")
finally:
self.queue.task_done() # Mark the task as done after processing
if self.delay > 0:
await asyncio.sleep(self.delay)
except asyncio.CancelledError:
# Exit silently on cancellation
break
except Exception as e:
self.bot.log.error(f"Worker error: {e}")
break
@command(permission="admin", options_first=True)
async def msgall(self, mask, target, args):
"""Send a message to all users in the channel using async queue
%%msgall <message>...
"""
if not target.is_channel:
return "This command can only be used in a channel"
message = ' '.join(args['<message>'])
workers = [] # Ensure workers is defined in the scope of the try block
try:
# Fetch the list of users in the channel
result = await self.bot.async_cmds.names(target)
nicknames = [strip_nick_prefix(n) for n in result['names']]
recipients = [n for n in nicknames if n != self.bot.nick]
if not recipients:
return "No valid recipients found"
total = len(recipients)
self.count = 0 # Reset the counter for this run
# Add all recipients to the queue
for nick in recipients:
await self.queue.put(nick)
# Create worker tasks
workers = [asyncio.create_task(self._worker(message, total)) for _ in range(1)]
# Send initial confirmation
self.bot.privmsg(target, f"Starting mass message to {total} users...")
# Wait for the queue to be fully processed
await self.queue.join()
# Cancel worker tasks after the queue is empty
for task in workers:
task.cancel()
# Wait for workers to finish cancellation
await asyncio.gather(*workers, return_exceptions=True)
return f"Mass message completed. Sent to {self.count}/{total} users."
except asyncio.CancelledError:
self.bot.log.info("Mass message command cancelled. Cleaning up workers.")
# Cancel any existing worker tasks
for task in workers:
task.cancel()
# Allow workers to handle cancellation
await asyncio.gather(*workers, return_exceptions=True)
# Re-raise the cancellation to inform the bot framework
raise
except Exception as e:
self.bot.log.error(f"Error in msgall: {e}")
return f"Mass message failed: {str(e)}"

View File

View File

@ -0,0 +1,109 @@
import irc3
from irc3.plugins.command import command
import asyncio
@irc3.plugin
class VoicePlugin:
def __init__(self, bot):
self.bot = bot
@command(permission='admin')
async def voice(self, mask, target, args):
"""Give voice to all users or a specific user
%%voice [<nick>]
"""
nick = args.get('<nick>')
if nick:
await self.give_voice(target, nick)
else:
await self.give_voice_all(target)
@command(permission='admin')
async def devoice(self, mask, target, args):
"""Remove voice from all users or a specific user
%%devoice [<nick>]
"""
nick = args.get('<nick>')
if nick:
await self.remove_voice(target, nick)
else:
await self.remove_voice_all(target)
async def give_voice(self, target, nick):
"""Give voice to a specific user"""
self.bot.send(f'MODE {target} +v {nick}')
async def remove_voice(self, target, nick):
"""Remove voice from a specific user"""
self.bot.send(f'MODE {target} -v {nick}')
async def give_voice_all(self, target):
"""Give voice to all users in the channel who currently don't have it"""
names = await self.bot.async_cmds.names(target)
for user in names['names']:
if not user.startswith("+") and not user.startswith("@"):
self.bot.send(f'MODE {target} +v {user}')
await asyncio.sleep(0.07) # To avoid flooding the server with commands
async def remove_voice_all(self, target):
"""Remove voice from all users in the channel"""
names = await self.bot.async_cmds.names(target)
for user in names['names']:
if user.startswith("+"):
self.bot.send(f'MODE {target} -v {user.lstrip("+")}')
await asyncio.sleep(0.07) # To avoid flooding the server with commands
@irc3.plugin
class KickPlugin:
def __init__(self, bot):
self.bot = bot
@command(permission='admin')
async def kick(self, mask, target, args):
"""Kick a specific user from the channel
%%kick <nick> [<reason>]
"""
nick = args.get('<nick>')
reason = args.get('<reason>') or 'Kicked by admin'
if nick:
await self.kick_user(target, nick, reason)
async def kick_user(self, target, nick, reason):
"""Kick a specific user from the channel using ChanServ"""
self.bot.send(f'PRIVMSG ChanServ :KICK {target} {nick} {reason}')
@irc3.plugin
class BanPlugin:
def __init__(self, bot):
self.bot = bot
@command(permission='admin')
async def ban(self, mask, target, args):
"""Ban a specific user from the channel
%%ban <nick>
"""
nick = args.get('<nick>')
if nick:
await self.ban_user(target, nick)
@command(permission='admin')
async def unban(self, mask, target, args):
"""Unban a specific user from the channel
%%unban <nick>
"""
nick = args.get('<nick>')
if nick:
await self.unban_user(target, nick)
async def ban_user(self, target, nick):
"""Ban a specific user from the channel"""
self.bot.send(f'MODE {target} +b {nick}')
async def unban_user(self, target, nick):
"""Unban a specific user from the channel"""
self.bot.send(f'MODE {target} -b {nick}')

View File

@ -0,0 +1,167 @@
from irc3.plugins.command import command
from irc3.plugins.cron import cron
import irc3
from irc3 import utils
import re
from collections import defaultdict, deque
import time
from asynchronous import AsyncEvents
@irc3.plugin
class AntiSpam:
"""IRC3 Anti-Spam Plugin with Auto-Ban for Repeat Offenders"""
def __init__(self, bot):
self.bot = bot
self.config = bot.config.get('antispam', {})
self.user_data = defaultdict(lambda: {
'messages': deque(maxlen=int(self.config.get('repeat_limit', 3))),
'timestamps': deque(maxlen=int(self.config.get('spam_limit', 5))),
'mentions': deque(maxlen=int(self.config.get('mention_limit', 2)))
})
self.kick_history = defaultdict(deque) # Track kick timestamps per user
self.exclude_list = ['ZodBot']
self.service_name = self.config.get('service_name', 'ChanServ')
self.who_channel = WhoChannel(bot) # Initialize WHO channel handler
async def get_user_modes(self, nick, channel):
"""Dynamically fetch user modes using WHO command."""
try:
result = await self.who_channel(channel=channel)
if result['success']:
for user in result['users']:
if user['nick'].lower() == nick.lower():
return user['modes']
except Exception as e:
self.bot.log.error(f"Error fetching user modes: {e}")
return ""
def is_spam(self, nick, message, channel):
"""Check if message meets spam criteria"""
user = self.user_data[nick.lower()]
now = time.time()
if len(message) > int(self.config.get('max_length', 300)):
return True
if message in user['messages']:
if len(user['messages']) == user['messages'].maxlen - 1:
return True
user['timestamps'].append(now)
if len(user['timestamps']) == user['timestamps'].maxlen:
if (now - user['timestamps'][0]) < 60:
return True
if self.bot.nick.lower() in message.lower():
user['mentions'].append(now)
if len(user['mentions']) == user['mentions'].maxlen:
if (now - user['mentions'][0]) < 60:
return True
user['messages'].append(message)
return False
@irc3.event(irc3.rfc.PRIVMSG)
async def monitor_messages(self, mask, event, target, data):
"""Handle incoming messages and check for spam"""
if target.startswith("#"):
nick = mask.nick
message = data
channel_name = target.lower()
if nick in self.exclude_list:
return
user_modes = await self.get_user_modes(nick, channel_name)
if user_modes:
if {'o', '%', 'h', '@'} & set(user_modes):
return
if self.is_spam(nick, message, channel_name):
print(f"SPAM {nick} - {user_modes}")
self.handle_spam(mask, message, channel_name)
def handle_spam(self, mask, message, channel):
"""Take action against spam, escalating to ban if kicked twice in 5 minutes"""
nick = mask.nick
current_time = time.time()
cutoff = current_time - 300 # 5 minutes ago
nick_lower = nick.lower()
user_kicks = self.kick_history[nick_lower]
# Filter recent kicks within the last 5 minutes
recent_kicks = [ts for ts in user_kicks if ts >= cutoff]
if len(recent_kicks) >= 2:
# Ban the user using hostmask
ban_mask = f'*!{mask.host}'
self.bot.send(f"MODE {channel} +b {ban_mask}")
self.bot.privmsg(
self.service_name,
f"KICK {channel} {nick} Banned for repeated spamming"
)
# Clear history and data
del self.kick_history[nick_lower]
self.user_data.pop(nick_lower, None)
else:
# Kick and record timestamp
self.bot.privmsg(
self.service_name,
f"KICK {channel} {nick} :stop spamming"
)
user_kicks.append(current_time)
self.user_data.pop(nick_lower, None)
@cron('* * * * *')
def clean_old_records(self):
"""Cleanup inactive users every minute"""
cutoff = time.time() - 300
to_remove = [
nick for nick, data in self.user_data.items()
if len(data['timestamps']) > 0 and data['timestamps'][-1] < cutoff
]
for nick in to_remove:
del self.user_data[nick]
def connection_made(self):
"""Initialize when bot connects"""
self.bot.log.info("Enhanced AntiSpam plugin loaded with kick-to-ban escalation")
class WhoChannel(AsyncEvents):
"""Handle WHO responses for a channel."""
send_line = 'WHO {channel}'
events = (
{
'match': (
r"(?i)^:\S+ 352 \S+ {channel} (?P<user>\S+) "
r"(?P<host>\S+) (?P<server>\S+) (?P<nick>\S+) "
r"(?P<modes>\S+) :(?P<hopcount>\S+) (?P<realname>.*)"
),
'multi': True
},
{
'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {channel} :.*",
'final': True
},
)
def process_results(self, results=None, **value):
"""Process WHO channel results into a user list."""
users = []
for res in results:
if 'retcode' in res:
value.update(res)
else:
res['mask'] = utils.IrcString('{nick}!{user}@{host}'.format(**res))
users.append(res)
value['users'] = users
value['success'] = value.get('retcode') == '315'
return value

62
plugins/unicode_say.py Normal file
View File

@ -0,0 +1,62 @@
import irc3
import random
from irc3.plugins.command import command
@irc3.plugin
class SayPlugin:
def __init__(self, bot):
self.bot = bot
@command
def say(self, mask, target, args):
"""Say command
%%say <channel> <message>...
"""
channel = args.get('<channel>')
message = ' '.join(args['<message>'])
if not channel or not message:
self.bot.privmsg(target, "Usage: !say <channel> <message>")
return
styled_message = self.style_message(message)
self.bot.privmsg(channel, styled_message)
def add_combining_characters(self, char):
combining_chars = [
'\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305',
'\u0306', '\u0307', '\u0308', '\u0309', '\u030A', '\u030B',
'\u030C', '\u030D', '\u030E', '\u030F', '\u0310', '\u0311',
'\u0312', '\u0313', '\u0314', '\u0315', '\u0316', '\u0317',
'\u0318', '\u0319', '\u031A', '\u031B', '\u031C', '\u031D',
'\u031E', '\u031F', '\u0320', '\u0321', '\u0322', '\u0323',
'\u0324', '\u0325', '\u0326', '\u0327', '\u0328', '\u0329',
'\u032A', '\u032B', '\u032C', '\u032D', '\u032E', '\u032F',
'\u0330', '\u0331', '\u0332', '\u0333', '\u0334', '\u0335',
'\u0336', '\u0337', '\u0338', '\u0339', '\u033A', '\u033B',
'\u033C', '\u033D', '\u033E', '\u033F', '\u0340', '\u0341',
'\u0342', '\u0343', '\u0344', '\u0345', '\u0346', '\u0347',
'\u0348', '\u0349', '\u034A', '\u034B', '\u034C', '\u034D',
'\u034E', '\u034F', '\u0350', '\u0351', '\u0352', '\u0353',
'\u0354', '\u0355', '\u0356', '\u0357', '\u0358', '\u0359',
'\u035A', '\u035B', '\u035C', '\u035D', '\u035E', '\u035F',
'\u0360', '\u0361', '\u0362'
]
glitched_char = char
for _ in range(random.randint(1, 1)):
color = random.randint(0, 15)
style = random.choice(['\x02', '\x1F']) # Bold and Underline
combining_char = random.choice(combining_chars)
glitched_char += f'\x03{color}{style}{combining_char}\x0F'
return glitched_char
def style_message(self, message):
white_color_code = '\x0300' # White color
styled_message = ''
for char in message:
char_with_combining = self.add_combining_characters(char)
styled_message += f'{white_color_code}{char_with_combining}'
return styled_message.strip() # Remove the trailing space

226
plugins/unicode_spam.py Normal file
View File

@ -0,0 +1,226 @@
import random
import string
import irc3
from irc3.plugins.command import command
import asyncio
import unicodedata
@irc3.plugin
class UnicodeSpammer:
def __init__(self, bot):
self.bot = bot
self.valid_code_points = self._generate_valid_code_points()
random.shuffle(self.valid_code_points)
# Dictionary to keep track of running spam tasks: {target_nick: asyncio.Task, ...}
self.spam_tasks = {}
# Task for periodically changing nick
self.nick_changer_task = None
# Store the original nickname to restore later.
self.original_nick = self.bot.nick
# Nick change interval in seconds; adjust as desired.
self.nick_change_interval = 10
def _generate_valid_code_points(self):
"""Generate a list of valid Unicode code points that are renderable and not Chinese."""
excluded_code_points = {
# Already excluded code points
0x1F95F, # 🮕
0x18F33, # 𘏳
0x18F34, # 𘏴
0x18F35, # 𘏵
*range(0x1FB8, 0x1FCD), # Range for the mentioned characters (🭨 to 🭿)
*range(0x1FD0, 0x1FF0), # Range for the mentioned characters (🯀 to 🯹)
*range(0x1DF20, 0x1DF33), # Range for 𐹠 to 𐹲
# New characters to exclude
0x1F200, 0x1F201, 0x1F202, 0x1F203, 0x1F204, 0x1F205, 0x1F206, 0x1F207,
0x1F208, 0x1F209, 0x1F20A, 0x1F20B, 0x1F20C, 0x1F20D, 0x1F20E, 0x1F20F,
0x1F210, 0x1F211, 0x1F212, 0x1F213, 0x1F214, 0x1F215, 0x1F216, 0x1F217,
0x1F218, 0x1F219, 0x1F21A, 0x1F21B, 0x1F21C, 0x1F21D, 0x1F21E, 0x1F21F,
0x1F220, 0x1F221, 0x1F222, 0x1F223, 0x1F224, 0x1F225, 0x1F226, 0x1F227,
0x1F228, 0x1F229, 0x1F22A, 0x1F22B, 0x1F22C, 0x1F22D, 0x1F22E, 0x1F22F,
0x1F230, 0x1F231, 0x1F232, 0x1F233, 0x1F234, 0x1F235, 0x1F236, 0x1F237,
0x1F238, 0x1F239, 0x1F23A, 0x1F23B, 0x1F23C, 0x1F23D, 0x1F23E, 0x1F23F,
0x1F240, 0x1F241, 0x1F242, 0x1F243, 0x1F244, 0x1F245, 0x1F246, 0x1F247,
0x1F248, 0x1F249, 0x1F24A, 0x1F24B, 0x1F24C, 0x1F24D, 0x1F24E, 0x1F24F,
0x1F250, 0x1F251, 0x1F252, 0x1F253, 0x1F254, 0x1F255, 0x1F256, 0x1F257,
0x1F258, 0x1F259, 0x1F25A, 0x1F25B, 0x1F25C, 0x1F25D, 0x1F25E, 0x1F25F,
# Additional code points to remove as per the original list
0x1F600, 0x1F601, 0x1F602, 0x1F603, 0x1F604, 0x1F605, 0x1F606, 0x1F607,
# ... (continue for any other exclusions you need) ...
# Unicode ranges to exclude (example ranges, adjust as needed)
*range(0x17004, 0x1707F),
*range(0x17154, 0x1717F),
*range(0x171D4, 0x171FF),
# ... many more ranges as in your original code ...
}
return [
cp for cp in range(0x110000)
if not self._is_chinese(cp) # Exclude Chinese characters
and self._is_renderable(cp) # Exclude non-renderable characters
and cp not in excluded_code_points # Exclude the specified characters
]
def _is_chinese(self, code_point):
"""Check if the Unicode code point is a Chinese character."""
try:
char = chr(code_point)
return unicodedata.name(char).startswith("CJK")
except ValueError:
return False
def _is_renderable(self, code_point):
"""Check if a Unicode code point is renderable in IRC."""
try:
char = chr(code_point)
except ValueError:
return False
category = unicodedata.category(char)
excluded_categories = {'Cc', 'Cf', 'Cs', 'Co', 'Cn', 'Zl', 'Zp', 'Mn', 'Mc', 'Me'}
return category not in excluded_categories
def _random_nick(self):
"""Generate a random nickname if no users are found."""
return 'User' + ''.join(random.choices(string.ascii_letters + string.digits, k=5))
async def _change_nick_periodically(self, channel):
"""
Every self.nick_change_interval seconds change the bot's nickname to mimic
a random user's nickname from the channel (with an underscore appended).
If no users are found, generate a random nickname.
"""
self.original_nick = self.bot.nick # store the original nick
try:
while True:
channel_key = channel.lower()
if channel_key in self.bot.channels:
users = list(self.bot.channels[channel_key])
if users:
random_user = random.choice(users)
new_nick = f"{random_user}_"
else:
new_nick = self._random_nick()
# Send the NICK command
self.bot.send(f'NICK {new_nick}')
self.bot.log.info(f"Nickname changed to mimic: {new_nick}")
self.bot.nick = new_nick
else:
self.bot.log.info(f"Channel {channel} not found for nick change.")
await asyncio.sleep(self.nick_change_interval)
except asyncio.CancelledError:
# Restore the original nickname upon cancellation.
self.bot.send(f'NICK {self.original_nick}')
self.bot.nick = self.original_nick
self.bot.log.info("Nickname reverted to original.")
raise
except Exception as e:
self.bot.log.error(f"Error changing nickname: {e}")
@command(permission='admin')
async def unicode(self, mask, target, args):
"""
Handle the !unicode command:
- !unicode <target>: start spamming Unicode messages to <target> and start periodic nick changes.
%%unicode [<target>]
"""
target_nick = args['<target>']
if target_nick in self.spam_tasks:
await self._send_message(target, f"Unicode spam is already running for {target_nick}.")
else:
if not target_nick:
target_nick = target
# Start the Unicode spam task for the target.
spam_task = asyncio.create_task(self._unicode_spam_loop(target_nick))
self.spam_tasks[target_nick] = spam_task
await self._send_message(target, f"Started Unicode spam for {target_nick}.")
# Start the nickname changer if not already running.
# We assume that if the command was issued in a channel,
# then `target` is the channel name.
if self.nick_changer_task is None:
self.nick_changer_task = asyncio.create_task(self._change_nick_periodically(target))
@command(permission='admin')
async def unicodestop(self, mask, target, args):
"""
Handle the !unicodestop command:
- !unicodestop: stop all running Unicode spamming and nick-changing tasks.
%%unicodestop [<target>]
"""
stop_msgs = []
# Cancel all running spam tasks.
if self.spam_tasks:
for nick, task in list(self.spam_tasks.items()):
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
del self.spam_tasks[nick]
stop_msgs.append("Stopped all Unicode spam tasks.")
else:
stop_msgs.append("No Unicode spam tasks are running.")
# Cancel the nickname changer task if it is running.
if self.nick_changer_task is not None:
self.nick_changer_task.cancel()
try:
await self.nick_changer_task
except asyncio.CancelledError:
pass
self.nick_changer_task = None
stop_msgs.append("Nickname changer stopped and nick reverted.")
await self._send_message(target, " ".join(stop_msgs))
async def _unicode_spam_loop(self, target_nick):
"""
Continuously send messages composed of Unicode characters to the given target.
This loop is cancelled when the !unicodestop command is issued.
"""
message_count = 0
try:
while True:
buffer = []
# Cycle through valid code points.
for code_point in self.valid_code_points:
try:
buffer.append(chr(code_point))
# When the buffer gets large, send a message.
if len(buffer) >= 400:
await self._send_message(target_nick, ''.join(buffer))
buffer.clear()
message_count += 1
await self._throttle_messages(message_count)
except Exception as e:
self.bot.log.error(f"Error processing character {code_point}: {e}")
# Send any remaining characters.
if buffer:
await self._send_message(target_nick, ''.join(buffer))
except asyncio.CancelledError:
self.bot.log.info(f"Unicode spam for {target_nick} cancelled.")
await self._send_message(target_nick, "Unicode spam stopped.")
raise
async def _send_message(self, target, message):
"""Send a message to the target with error handling."""
try:
self.bot.privmsg(target, message)
except Exception as e:
self.bot.log.error(f"Error sending message to {target}: {e}")
async def _throttle_messages(self, message_count):
"""Throttle messages to prevent flooding."""
await asyncio.sleep(0.007)
if message_count % 5 == 0:
await asyncio.sleep(0.07)
if message_count % 25 == 0:
await asyncio.sleep(0.07)
if message_count % 50 == 0:
await asyncio.sleep(0.07)

216
plugins/upload.py Normal file
View File

@ -0,0 +1,216 @@
import aiohttp
import aiofiles
import irc3
import tempfile
import os
import re
import asyncio
from irc3.plugins.command import command
import ircstyle
import yt_dlp
from yt_dlp.utils import DownloadError
from urllib.parse import urlparse
@irc3.plugin
class UploadPlugin:
"""IRC bot plugin for uploading files to hardfiles.org using yt-dlp for downloads."""
def __init__(self, bot):
self.bot = bot
@command
async def upload(self, mask, target, args):
"""
Upload a file to hardfiles.org (Max 100MB).
%%upload [--mp3] <url>
"""
url = args.get('<url>')
mp3 = args.get('--mp3')
if not url:
self.bot.privmsg(
target,
ircstyle.style("Usage: !upload [--mp3] <url>", fg="red", bold=True, reset=True)
)
return
try:
# Directly await the upload task.
await self.do_upload(url, target, mp3)
except Exception as exc:
self.bot.privmsg(
target,
ircstyle.style(f"Upload task error: {exc}", fg="red", bold=True, reset=True)
)
async def do_upload(self, url, target, mp3):
"""Download a file using yt-dlp and upload it."""
max_size = 100 * 1024 * 1024 # 100MB limit
# Use a temporary directory context manager so cleanup is automatic.
with tempfile.TemporaryDirectory() as tmp_dir:
# Parse URL and determine if a header check is needed.
parsed_url = urlparse(url)
domain = parsed_url.netloc.lower()
skip_check_domains = ("x.com", "instagram.com", "youtube.com", "youtu.be", "streamable.com")
should_check_headers = not any(domain.endswith(d) for d in skip_check_domains)
if should_check_headers:
async with aiohttp.ClientSession() as session:
async with session.head(url) as response:
if response.status != 200:
self.bot.privmsg(
target,
ircstyle.style(f"Failed to fetch headers: HTTP {response.status}", fg="red", bold=True, reset=True)
)
return
content_length = response.headers.get('Content-Length')
if content_length and int(content_length) > max_size:
self.bot.privmsg(
target,
ircstyle.style(
f"File size ({int(content_length) // (1024 * 1024)}MB) exceeds 100MB limit",
fg="red", bold=True, reset=True
)
)
return
# Set up yt-dlp options.
ydl_opts = {
'outtmpl': os.path.join(tmp_dir, '%(title)s.%(ext)s'),
'format': 'bestaudio/best' if mp3 else 'best[ext=mp4]/best',
'restrictfilenames': True,
'noplaylist': True,
'quiet': True,
'concurrent_fragment_downloads': 5,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}] if mp3 else [],
}
# Use yt_dlp to extract information (first without downloading).
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
try:
info = await asyncio.to_thread(ydl.extract_info, url, download=False)
except DownloadError as e:
self.bot.privmsg(
target,
ircstyle.style(f"Info extraction failed: {e}", fg="red", bold=True, reset=True)
)
return
# Check the estimated file size if available.
estimated_size = info.get('filesize') or info.get('filesize_approx')
if estimated_size and estimated_size > max_size:
self.bot.privmsg(
target,
ircstyle.style(
f"File size ({estimated_size // (1024 * 1024)}MB) exceeds 100MB limit",
fg="red", bold=True, reset=True
)
)
return
# Proceed with the download.
try:
info = await asyncio.to_thread(ydl.extract_info, url, download=True)
except DownloadError as e:
self.bot.privmsg(
target,
ircstyle.style(f"Download failed: {e}", fg="red", bold=True, reset=True)
)
return
# Prepare and send metadata (if available).
metadata_parts = []
title = info.get("title")
uploader = info.get("uploader")
duration = info.get("duration")
upload_date = info.get("upload_date")
view_count = info.get("view_count")
description = info.get("description")
if title:
metadata_parts.append(ircstyle.style(f"Title: {title}", fg="yellow", bold=True, reset=True))
if uploader:
metadata_parts.append(ircstyle.style(f"Uploader: {uploader}", fg="purple", bold=True, reset=True))
if duration:
metadata_parts.append(ircstyle.style(f"Duration: {self._format_duration(duration)}", fg="green", bold=True, reset=True))
if upload_date:
# Format date as YYYY-MM-DD if possible.
formatted_date = f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:]}" if len(upload_date) == 8 else upload_date
metadata_parts.append(ircstyle.style(f"Upload Date: {formatted_date}", fg="aqua", bold=True, reset=True))
if view_count is not None:
metadata_parts.append(ircstyle.style(f"Views: {view_count}", fg="royal", bold=True, reset=True))
if description:
if len(description) > 200:
description = description[:200] + "..."
metadata_parts.append(ircstyle.style(f"Description: {description}", fg="silver", reset=True))
if metadata_parts:
self.bot.privmsg(target, " | ".join(metadata_parts))
# Verify that a file was downloaded.
downloaded_files = info.get('requested_downloads', [])
if not downloaded_files:
self.bot.privmsg(
target,
ircstyle.style("No files downloaded", fg="red", bold=True, reset=True)
)
return
# Retrieve the file path.
first_file = downloaded_files[0]
downloaded_file = first_file.get('filepath', first_file.get('filename'))
if not downloaded_file or not os.path.exists(downloaded_file):
self.bot.privmsg(
target,
ircstyle.style(f"Downloaded file not found: {downloaded_file}", fg="red", bold=True, reset=True)
)
return
# Check the actual file size as an extra safeguard.
file_size = os.path.getsize(downloaded_file)
if file_size > max_size:
self.bot.privmsg(
target,
ircstyle.style(
f"File size ({file_size // (1024 * 1024)}MB) exceeds 100MB limit",
fg="red", bold=True, reset=True
)
)
return
# Upload the file to hardfiles.org.
async with aiohttp.ClientSession() as session:
form = aiohttp.FormData()
async with aiofiles.open(downloaded_file, 'rb') as f:
file_content = await f.read()
form.add_field('file', file_content, filename=os.path.basename(downloaded_file))
async with session.post('https://hardfiles.org/', data=form, allow_redirects=False) as resp:
if resp.status not in [200, 201, 302, 303]:
self.bot.privmsg(
target,
ircstyle.style(f"Upload failed: HTTP {resp.status}", fg="red", bold=True, reset=True)
)
else:
response_text = await resp.text()
upload_url = self.extract_url_from_response(response_text) or "Unknown URL"
response_msg = (
ircstyle.style("Upload successful: ", fg="green", bold=True, reset=True) +
ircstyle.style(upload_url, fg="blue", underline=True, reset=True)
)
self.bot.privmsg(target, response_msg)
def extract_url_from_response(self, response_text):
"""Extract the first URL found in the response text."""
match = re.search(r'https?://\S+', response_text)
return match.group(0) if match else None
def _format_duration(self, seconds):
"""Convert seconds into a human-readable Hh Mm Ss format."""
seconds = int(seconds)
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
return f"{h}h {m}m {s}s" if h else f"{m}m {s}s"

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from irc3.plugins.command import command
from irc3.compat import Queue
import irc3
import aiohttp
@irc3.plugin
class UrbanDictionaryPlugin:
"""
A plugin to search Urban Dictionary for terms and post results to IRC.
This plugin uses a queue to manage asynchronous search requests and ensures
proper resource cleanup when unloaded.
"""
def __init__(self, bot):
self.bot = bot
self.queue = Queue() # Queue for managing search requests
self.session = None # aiohttp session initialized lazily
self.bot.loop.create_task(self._process_queue()) # Start queue processor
@command(permission='view', aliases=['ud'], public=True)
def urban(self, mask, target, args):
"""
Search Urban Dictionary for a term.
%%urban <term>...
"""
term = ' '.join(args['<term>'])
if not term:
return "Please provide a term to search."
# Enqueue the search request
self.queue.put_nowait((term, target))
return f"\x02Searching Urban Dictionary\x02 for '\x034{term}\x03'..."
async def _process_queue(self):
"""
Process search requests from the queue asynchronously.
This method runs indefinitely, processing one request at a time.
"""
while True:
try:
# Get the next search request
term, target = await self.queue.get()
# Ensure the session is initialized
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession(loop=self.bot.loop)
# Fetch data from Urban Dictionary API
url = f"https://api.urbandictionary.com/v0/define?term={term}"
async with self.session.get(url) as response:
if response.status != 200:
self.bot.privmsg(target, f"\x02Error:\x02 Failed to fetch definition for '\x034{term}\x03'.")
continue
data = await response.json()
if not data.get('list'):
self.bot.privmsg(target, f"\x02No definition found\x02 for '\x034{term}\x03'.")
continue
# Extract the first result
result = data['list'][0]
definition = result['definition'].replace('\n', ' ').strip()
example = result['example'].replace('\n', ' ').strip()
permalink = result['permalink']
# Format the response with colors and bold
response_message = (
f"\x02[{term}]\x02 \x033{definition}\x03 | "
f"Example: \x0312{example}\x03 | "
f"More: \x032{permalink}\x03"
)
# Send the response to the IRC channel
self.bot.privmsg(target, response_message)
except Exception as e:
self.bot.privmsg(target, f"\x02Error:\x02 An unexpected error occurred: {str(e)}")
finally:
# Mark the task as done
self.queue.task_done()
def __unload__(self):
"""
Cleanup when the plugin is unloaded.
Ensures that the aiohttp session is properly closed to avoid resource leaks.
"""
if self.session and not self.session.closed:
self.bot.loop.create_task(self.session.close())

View File

@ -0,0 +1,132 @@
"""
A plugin for fetching and displaying titles of URLs shared in IRC messages.
"""
import re
import asyncio
import aiohttp
from lxml import html
import irc3
from irc3 import event
from irc3.compat import Queue
class URLTitlePlugin:
"""
A plugin to fetch and display the titles of URLs shared in IRC messages.
Attributes:
bot (irc3.IrcBot): The IRC bot instance.
url_queue (Queue): A queue to manage URL processing asynchronously.
session (aiohttp.ClientSession): An HTTP session for making requests.
"""
def __init__(self, bot):
"""
Initialize the URLTitlePlugin.
Args:
bot (irc3.IrcBot): The IRC bot instance.
"""
self.bot = bot
self.url_queue = Queue()
self.session = aiohttp.ClientSession(loop=self.bot.loop)
self.bot.create_task(self.process_urls())
@event(irc3.rfc.PRIVMSG)
async def on_privmsg(self, mask, event, target, data):
"""
Listen for PRIVMSG events and check for URLs.
Args:
mask (str): The user's mask (e.g., nick!user@host).
event (str): The IRC event type (e.g., PRIVMSG).
target (str): The target of the message (e.g., channel or user).
data (str): The content of the message.
This method extracts URLs from the message and adds them to the queue
for asynchronous processing.
"""
url_pattern = re.compile(r"https?://[^\s<>\"']+|www\.[^\s<>\"']+")
urls = url_pattern.findall(data)
for url in urls:
# Use put_nowait to avoid blocking if the queue is full
await self.url_queue.put((url, target))
async def process_urls(self):
"""
Process URLs from the queue and fetch their titles.
This method runs indefinitely, processing one URL at a time from the queue.
It fetches the title of each URL and sends it back to the IRC channel.
"""
while True:
url, target = await self.url_queue.get()
try:
title = await self.fetch_title(url)
if title:
# Format the IRC message with colors and styles
formatted_message = (
f"\x02\x0312Title:\x03 \x034{title}\x03 \x02|\x02 "
f"\x032URL:\x03 \x0311{url}\x03"
)
await self.bot.privmsg(target, formatted_message)
else:
# Format the error message with colors and styles
# formatted_message = (
# f"\x02\x034Error:\x03 Could not find a title for: "
# f"\x0311{url}\x03"
# )
# await self.bot.privmsg(target, formatted_message)
pass
except Exception as e:
self.bot.log.error(f"Error processing URL {url}: {e}")
finally:
self.url_queue.task_done()
async def fetch_title(self, url):
"""
Fetch the title of a web page using aiohttp and lxml.
Args:
url (str): The URL of the web page.
Returns:
str: The title of the web page, or None if it could not be fetched.
This method makes an HTTP GET request to the URL, parses the HTML content,
and extracts the title element.
"""
headers = {"User-Agent": "Mozilla/5.0"}
try:
async with self.session.get(url, headers=headers, timeout=10) as response:
# Check if the response was successful
response.raise_for_status()
content = await response.text()
tree = html.fromstring(content)
title = tree.findtext(".//title")
return title.strip() if title else "No title found"
except aiohttp.ClientError as e:
self.bot.log.error(f"HTTP error for {url}: {e}")
except asyncio.TimeoutError:
self.bot.log.error(f"Request timed out for {url}")
except Exception as e:
self.bot.log.error(f"Unexpected error for {url}: {e}")
return None
async def close(self):
"""
Clean up resources when the plugin is unloaded.
This method ensures that the aiohttp session is properly closed to avoid
resource leaks.
"""
await self.session.close()
def __del__(self):
"""
Ensure session closing when the object is destroyed.
This method schedules the session cleanup task on the bot's event loop.
"""
self.bot.create_task(self.close())

203
plugins/users.py Normal file
View File

@ -0,0 +1,203 @@
from __future__ import annotations
import logging
from irc3 import plugin, utils, rfc
from irc3.dec import event
from irc3.utils import IrcString
from collections import defaultdict
from typing import Optional, Set, Dict, Any, cast
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Channel(set):
"""Represents an IRC channel with member management and mode tracking."""
def __init__(self) -> None:
super().__init__()
self.modes: Dict[str, Set[str]] = defaultdict(set)
self.topic: Optional[str] = None
def add_member(self, nick: str, modes: str = '') -> None:
"""Add member with optional modes."""
super().add(nick)
self._update_modes(nick, modes, add=True)
logger.debug(f"Added member {nick} with modes {modes} to channel")
def remove_member(self, nick: str) -> None:
"""Remove member and all associated modes."""
super().discard(nick)
self._update_modes(nick, remove=True)
logger.debug(f"Removed member {nick} from channel")
def _update_modes(self, nick: str, modes: str = '',
add: bool = False, remove: bool = False) -> None:
"""Update mode tracking for a member."""
for mode in modes:
if add:
self.modes[mode].add(nick)
elif remove:
self.modes[mode].discard(nick)
logger.debug(f"Updated modes for {nick}: {'Added' if add else 'Removed'} {modes}")
def __repr__(self) -> str:
return f"Channel({sorted(self)})"
@plugin
class Userlist:
"""Enhanced user tracking with proper hostmask handling and mode management."""
def __init__(self, context: Any) -> None:
self.context = context
self.connection_lost()
def connection_lost(self, client: Any = None) -> None:
"""Reset state on connection loss."""
self.channels: Dict[str, Channel] = defaultdict(Channel)
self.nicks: Dict[str, IrcString] = {}
self.context.channels = self.channels
self.context.nicks = self.nicks
logger.info("Connection lost, state reset.")
@event(rfc.JOIN_PART_QUIT)
def on_join_part_quit(self, mask: Optional[IrcString] = None,
event: Optional[str] = None, **kwargs: Any) -> None:
"""Handle join/part/quit events."""
if mask and event:
getattr(self, event.lower())(mask.nick, mask, **kwargs)
logger.debug(f"Event {event} for {mask.nick}")
@event(rfc.KICK)
def on_kick(self, mask: Optional[IrcString] = None, target: Optional[IrcString] = None,
**kwargs: Any) -> None:
"""Handle kick events."""
if target:
self.part(target.nick, mask, **kwargs) # Note: Added mask here for consistency
logger.debug(f"User {target.nick} was kicked from a channel")
def join(self, nick: str, mask: IrcString, channel: str, **kwargs: Any) -> None:
"""Process user join."""
channel_obj = self.channels[channel]
if nick != self.context.nick:
channel_obj.add_member(mask.nick)
self.nicks[mask.nick] = mask
self._broadcast(channel_obj, **kwargs)
logger.debug(f"User {nick} joined channel {channel}")
def part(self, nick: str, mask: IrcString, channel: str, **kwargs: Any) -> None:
"""Process user part."""
if nick == self.context.nick:
self.channels.pop(channel, None)
logger.debug(f"Left channel {channel}")
else:
channel_obj = self.channels.get(channel)
if channel_obj:
self._broadcast(channel_obj, **kwargs)
channel_obj.remove_member(nick)
self._cleanup_nick(nick)
logger.debug(f"User {nick} parted from channel {channel}")
def quit(self, nick: str, mask: IrcString, **kwargs: Any) -> None:
"""Process user quit."""
if nick == self.context.nick:
self.connection_lost()
logger.info(f"User {nick} quit, connection lost")
else:
affected = set()
for channel in self.channels.values():
if nick in channel:
channel.remove_member(nick)
affected.update(channel)
self._broadcast(affected, **kwargs)
self._cleanup_nick(nick)
logger.debug(f"User {nick} quit from channels: {affected}")
@event(rfc.NEW_NICK)
def new_nick(self, nick: IrcString, new_nick: str, **kwargs: Any) -> None:
"""Handle nickname changes."""
host = nick.host or ''
self.nicks[new_nick] = IrcString(f"{new_nick}!{host}")
old_nick = nick.nick
self._update_nick_in_channels(old_nick, new_nick)
self.nicks.pop(old_nick, None)
self._broadcast({new_nick}, **kwargs)
logger.debug(f"Nickname change: {old_nick} -> {new_nick}")
def _update_nick_in_channels(self, old_nick: str, new_nick: str) -> None:
"""Update nickname across all channels."""
for channel in self.channels.values():
if old_nick in channel:
channel.remove_member(old_nick)
channel.add_member(new_nick)
for mode_set in channel.modes.values():
if old_nick in mode_set:
mode_set.add(new_nick)
mode_set.discard(old_nick)
logger.debug(f"Updated nickname {old_nick} to {new_nick} in channels")
@event(rfc.RPL_NAMREPLY)
def names(self, channel: str, data: str, **kwargs: Any) -> None:
"""Process NAMES reply with proper mode handling."""
statusmsg = self.context.server_config.get('STATUSMSG', '@+')
prefix_config = self.context.server_config.get('PREFIX', '(ov)@+')
mode_chars, prefixes = self._parse_prefix_config(prefix_config)
prefix_map = dict(zip(prefixes, mode_chars))
channel_obj = self.channels[channel]
for item in data.split():
nick = item.lstrip(statusmsg)
modes = ''.join([prefix_map[p] for p in item[:len(item)-len(nick)]
if p in prefix_map])
channel_obj.add_member(nick, modes)
logger.debug(f"Processed NAMES reply for channel {channel}")
def _parse_prefix_config(self, config: str) -> tuple[str, str]:
"""Parse PREFIX config into mode characters and symbols."""
parts = config.strip('()').split(')')
return (parts[0], parts[1]) if len(parts) == 2 else ('ov', '@+')
@event(rfc.RPL_WHOREPLY)
def who(self, channel: str, nick: str, username: str,
server: str, **kwargs: Any) -> None:
"""Process WHO reply with hostmask."""
mask = IrcString(f"{nick}!{username}@{server}")
self.channels[channel].add_member(nick)
self.nicks[nick] = mask
logger.debug(f"Processed WHO reply for {nick} in {channel}")
@event(rfc.MODE)
def mode(self, target: str, modes: str, data: Any, **kwargs: Any) -> None:
"""Handle mode changes with proper parameter handling."""
chantypes = self.context.server_config.get('CHANTYPES', '#&')
if not target or target[0] not in chantypes:
return
prefix_config = self.context.server_config.get('PREFIX', '(ov)@+')
mode_chars, prefixes = self._parse_prefix_config(prefix_config)
param_modes = self.context.server_config.get('CHANMODES', '').split(',')[0]
parsed = utils.parse_modes(modes, data.split() if isinstance(data, str) else data)
channel = self.channels[target]
for sign, mode, param in parsed:
if mode in param_modes and param:
# Handle parameterized modes (e.g., +b, +k)
pass
elif mode in mode_chars:
prefix = dict(zip(mode_chars, prefixes))
mode_key = prefix.get(mode, '')
if sign == '+':
channel.modes[mode_key].add(param)
else:
channel.modes[mode_key].discard(param)
logger.debug(f"Mode change in {target}: {modes}")
def _cleanup_nick(self, nick: str) -> None:
"""Remove nick if not in any channels."""
if not any(nick in channel for channel in self.channels.values()):
self.nicks.pop(nick, None)
logger.debug(f"Cleaned up nick {nick} as no longer in any channels")
def _broadcast(self, targets: Any, **kwargs: Any) -> None:
"""Placeholder for broadcast functionality."""
logger.debug("Broadcasting to targets: %s", targets)

56
plugins/whois.py Normal file
View File

@ -0,0 +1,56 @@
import irc3
import re
from irc3 import dec
@irc3.plugin
class AsyncNotifyOnJoin:
def __init__(self, bot):
self.bot = bot
# Ensure async_cmds is available
if not hasattr(self.bot, 'async_cmds'):
self.bot.log.error("async_cmds plugin not loaded. Please add 'irc3.plugins.asynchronious' to your config.")
raise RuntimeError("Missing async_cmds plugin")
@dec.extend
async def notify_users(self, channel, joiner):
"""Async method to get channel users and send notifications"""
try:
# Ensure channel is in the bot's channel list
if channel not in self.bot.channels:
self.bot.log.warning(f"Bot is not in channel: {channel}")
return
# Debug: Log the async_cmds object
self.bot.log.debug(f"async_cmds: {self.bot.async_cmds}")
# Debug: Log the names method
if hasattr(self.bot.async_cmds, 'names'):
self.bot.log.debug("async_cmds.names() is available")
else:
self.bot.log.error("async_cmds.names() is missing!")
# Get fresh user list through IRC protocol
result = await self.bot.async_cmds.names(channel)
self.bot.log.debug(f"Result from async_cmds.names(): {result}")
if result and result.get('success'):
for nickname in result['names']:
# Clean nickname from mode prefixes
clean_nick = re.sub(r'^[@~+&%]', '', nickname)
if clean_nick != self.bot.nick:
self.bot.privmsg(
clean_nick,
f"{joiner} has joined {channel}. Welcome them!"
)
else:
self.bot.log.error(f"Failed to fetch user list for {channel}")
except Exception as e:
self.bot.log.error(f"Notification failed: {e}")
@irc3.event(irc3.rfc.JOIN)
def on_join(self, channel, **kwargs):
"""Trigger async notification process on join"""
joiner = kwargs['mask'].nick
if joiner != self.bot.nick:
# Create async task to handle notifications
self.bot.loop.create_task(self.notify_users(channel, joiner))