initial commit
This commit is contained in:
commit
dcf7b980a9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.pyc
|
||||||
|
|
123
asynchronous.py
Normal file
123
asynchronous.py
Normal 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
110
config.ini
Normal 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
23
goat.txt
Normal 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
0
plugins/__init__.py
Normal file
479
plugins/asynchronious.py
Normal file
479
plugins/asynchronious.py
Normal 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
169
plugins/bomb.py
Normal 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
51
plugins/disregard.py
Normal 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
71
plugins/goat.py
Normal 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
90
plugins/imitate.py
Normal 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
50
plugins/matrix.py
Normal 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
0
plugins/monkey_patch.py
Normal file
220
plugins/my_yt.py
Normal file
220
plugins/my_yt.py
Normal 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
102
plugins/random_msg.py
Normal 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)}"
|
0
plugins/services/__init__.py
Normal file
0
plugins/services/__init__.py
Normal file
109
plugins/services/administration.py
Normal file
109
plugins/services/administration.py
Normal 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}')
|
167
plugins/services/anti_spam.py
Normal file
167
plugins/services/anti_spam.py
Normal 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
62
plugins/unicode_say.py
Normal 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
226
plugins/unicode_spam.py
Normal 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
216
plugins/upload.py
Normal 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"
|
90
plugins/urban_dictionary.py
Normal file
90
plugins/urban_dictionary.py
Normal 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())
|
132
plugins/url_title_sniffer.py
Normal file
132
plugins/url_title_sniffer.py
Normal 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
203
plugins/users.py
Normal 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
56
plugins/whois.py
Normal 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))
|
Loading…
Reference in New Issue
Block a user