clean up code
This commit is contained in:
parent
75d830b260
commit
3d82132425
@ -1,123 +1,88 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
:mod:`irc3.plugins.asynchronous` Asynchronous Events
|
||||||
======================================================
|
======================================================
|
||||||
|
|
||||||
This module provide a way to catch data from various predefined events.
|
Provides asynchronous handling for various IRC events including WHOIS, WHO queries,
|
||||||
|
and channel management through non-blocking operations.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- **WHOIS Support**: Retrieve detailed user information from the server.
|
||||||
|
- **WHO Queries**: Fetch channel users with their respective flags.
|
||||||
|
- **CTCP Handling**: Manage custom Client-to-Client Protocol requests.
|
||||||
|
- **Channel Topic Management**: Get or modify channel topics efficiently.
|
||||||
|
- **Ban List Handling**: Query active bans on a channel.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
Subclass `~irc3.asynchronous.AsyncEvents` to create custom asynchronous event handlers.
|
||||||
|
|
||||||
You'll have to define a subclass of :class:`~irc3.asynchronous.AsyncEvents`:
|
Example:
|
||||||
|
|
||||||
.. 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:
|
class MyPlugin:
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.whois = Whois(bot)
|
self.whois = Whois(bot)
|
||||||
|
|
||||||
def do_whois(self):
|
async def do_whois(self):
|
||||||
# remember {nick} in the regexp? Here it is
|
|
||||||
whois = await self.whois(nick='gawel')
|
whois = await self.whois(nick='gawel')
|
||||||
if int(whois['idle']) / 60 > 10:
|
if int(whois['idle']) / 60 > 10:
|
||||||
self.bot.privmsg('gawel', 'Wake up dude')
|
self.bot.privmsg('gawel', 'Wake up dude')
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
Always verify `result['timeout']` to ensure a response was received before the timeout.
|
||||||
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:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from irc3.asynchronous import AsyncEvents
|
||||||
|
from irc3 import utils
|
||||||
|
from irc3 import dec
|
||||||
|
|
||||||
|
|
||||||
class Whois(AsyncEvents):
|
class Whois(AsyncEvents):
|
||||||
"""Asynchronously handle WHOIS responses from the IRC server."""
|
"""Asynchronously handle WHOIS responses to gather user details.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
timeout (int): Default timeout for WHOIS response in seconds.
|
||||||
|
send_line (str): IRC command template for WHOIS requests.
|
||||||
|
"""
|
||||||
|
|
||||||
# Command timeout in seconds
|
|
||||||
timeout = 20
|
timeout = 20
|
||||||
|
|
||||||
# Line sent to trigger WHOIS
|
|
||||||
send_line = 'WHOIS {nick} {nick}'
|
send_line = 'WHOIS {nick} {nick}'
|
||||||
|
|
||||||
# Regex patterns to match server responses
|
|
||||||
events = (
|
events = (
|
||||||
{'match': r"(?i)^:\S+ 301 \S+ {nick} :(?P<away>.*)"},
|
{'match': r"(?i)^:\S+ 301 \S+ {nick} :(?P<away>.*)"},
|
||||||
{
|
{
|
||||||
'match': (
|
'match': (
|
||||||
r"(?i)^:\S+ 311 \S+ {nick} (?P<username>\S+) "
|
r"(?i)^:\S+ 311 \S+ {nick} (?P<username>\S+) (?P<host>\S+) "
|
||||||
r"(?P<host>\S+) . :(?P<realname>.*)"
|
r". :(?P<realname>.*)"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'match': (
|
'match': r"(?i)^:\S+ 312 \S+ {nick} (?P<server>\S+) :(?P<server_desc>.*)"
|
||||||
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+ 317 \S+ {nick} (?P<idle>[0-9]+).*"},
|
||||||
{
|
{'match': r"(?i)^:\S+ 319 \S+ {nick} :(?P<channels>.*)", 'multi': True},
|
||||||
'match': r"(?i)^:\S+ 319 \S+ {nick} :(?P<channels>.*)",
|
|
||||||
'multi': True
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'match': (
|
'match': (
|
||||||
r"(?i)^:\S+ 330 \S+ {nick} (?P<account>\S+) "
|
r"(?i)^:\S+ 330 \S+ {nick} (?P<account>\S+) :(?P<account_desc>.*)"
|
||||||
r":(?P<account_desc>.*)"
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{'match': r"(?i)^:\S+ 671 \S+ {nick} :(?P<connection>.*)"},
|
{'match': r"(?i)^:\S+ 671 \S+ {nick} :(?P<connection>.*)"},
|
||||||
{
|
{
|
||||||
'match': (
|
'match': r"(?i)^:\S+ (?P<retcode>(318|401)) \S+ (?P<nick>{nick}) :.*",
|
||||||
r"(?i)^:\S+ (?P<retcode>(318|401)) \S+ (?P<nick>{nick}) :.*"
|
'final': True,
|
||||||
),
|
|
||||||
'final': True
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_results(self, results=None, **value):
|
def process_results(self, results=None, **value):
|
||||||
"""Process WHOIS results into a structured dictionary.
|
"""Aggregate and structure WHOIS results into a consolidated dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
results (list): List of event results.
|
results (list): Collected event responses.
|
||||||
**value: Accumulated results.
|
**value: Accumulated data from event processing.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Processed WHOIS data with channels, success flag, etc.
|
dict: Structured user information with success status.
|
||||||
"""
|
"""
|
||||||
channels = []
|
channels = []
|
||||||
for res in results:
|
for res in results:
|
||||||
@ -129,351 +94,93 @@ class Whois(AsyncEvents):
|
|||||||
|
|
||||||
|
|
||||||
class WhoChannel(AsyncEvents):
|
class WhoChannel(AsyncEvents):
|
||||||
"""Handle WHO responses for a channel."""
|
"""Handle WHO responses for channel user listings.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
send_line (str): IRC command template for WHO requests.
|
||||||
|
"""
|
||||||
|
|
||||||
send_line = 'WHO {channel}'
|
send_line = 'WHO {channel}'
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
{
|
{
|
||||||
'match': (
|
'match': (
|
||||||
r"(?i)^:\S+ 352 \S+ {channel} (?P<user>\S+) "
|
r"(?i)^:\S+ 352 \S+ {channel} (?P<user>\S+) (?P<host>\S+) "
|
||||||
r"(?P<host>\S+) (?P<server>\S+) (?P<nick>\S+) "
|
r"(?P<server>\S+) (?P<nick>\S+) (?P<modes>\S+) "
|
||||||
r"(?P<modes>\S+) :(?P<hopcount>\S+) (?P<realname>.*)"
|
r":(?P<hopcount>\S+) (?P<realname>.*)"
|
||||||
),
|
),
|
||||||
'multi': True
|
'multi': True,
|
||||||
},
|
|
||||||
{
|
|
||||||
'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {channel} :.*",
|
|
||||||
'final': True
|
|
||||||
},
|
},
|
||||||
|
{'match': r"(?i)^:\S+ (?P<retcode>(315|401)) \S+ {channel} :.*", 'final': True},
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_results(self, results=None, **value):
|
def process_results(self, results=None, **value):
|
||||||
"""Process WHO channel results into a user list."""
|
"""Compile WHO channel results into a list of users.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results (list): Raw event response data.
|
||||||
|
**value: Extracted key-value pairs from responses.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Processed result with user list and success status.
|
||||||
|
"""
|
||||||
users = []
|
users = []
|
||||||
for res in results:
|
for res in results:
|
||||||
if 'retcode' in res:
|
if 'retcode' in res:
|
||||||
value.update(res)
|
value.update(res)
|
||||||
else:
|
else:
|
||||||
res['mask'] = utils.IrcString('{nick}!{user}@{host}'.format(**res))
|
res['mask'] = utils.IrcString(f"{res['nick']}!{res['user']}@{res['host']}")
|
||||||
users.append(res)
|
users.append(res)
|
||||||
value['users'] = users
|
value['users'] = users
|
||||||
value['success'] = value.get('retcode') == '315'
|
value['success'] = value.get('retcode') == '315'
|
||||||
return value
|
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
|
@dec.plugin
|
||||||
class Async:
|
class Async:
|
||||||
"""Provide asynchronous commands for IRC interactions.
|
"""Expose asynchronous IRC command interfaces for plugin usage."""
|
||||||
Extends the bot with methods using AsyncEvents for handling server responses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
|
"""Initialize with the bot context and register async commands."""
|
||||||
self.context = context
|
self.context = context
|
||||||
self.context.async_cmds = self
|
self.context.async_cmds = self
|
||||||
self.async_whois = Whois(context)
|
self.async_whois = Whois(context)
|
||||||
self.async_who_channel = WhoChannel(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):
|
def send_message(self, target: str, message: str):
|
||||||
"""Send a message asynchronously"""
|
"""Send a message to a target (channel or user).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): Recipient channel or nickname.
|
||||||
|
message (str): Message content to send.
|
||||||
|
"""
|
||||||
self.context.privmsg(target, message)
|
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
|
@dec.extend
|
||||||
def whois(self, nick, timeout=20):
|
def whois(self, nick: str, timeout: int = 20):
|
||||||
"""Send a WHOIS and return a Future with received data.
|
"""Initiate a WHOIS query for a nickname.
|
||||||
|
|
||||||
Example:
|
Args:
|
||||||
result = await bot.async_cmds.whois('gawel')
|
nick (str): Nickname to query.
|
||||||
|
timeout (int): Response timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Awaitable[dict]: WHOIS result data.
|
||||||
"""
|
"""
|
||||||
return self.async_whois(nick=nick.lower(), timeout=timeout)
|
return self.async_whois(nick=nick.lower(), timeout=timeout)
|
||||||
|
|
||||||
@dec.extend
|
@dec.extend
|
||||||
def who(self, target, flags=None, timeout=20):
|
def who(self, target: str, timeout: int = 20):
|
||||||
"""Send a WHO and return a Future with received data.
|
"""Perform a WHO query on a channel or user.
|
||||||
|
|
||||||
Examples:
|
Args:
|
||||||
result = await bot.async_cmds.who('gawel')
|
target (str): Channel or nickname to query.
|
||||||
result = await bot.async_cmds.who('#irc3', 'an')
|
timeout (int): Response timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Awaitable[dict] | None: WHO results for channels, else None.
|
||||||
"""
|
"""
|
||||||
target = target.lower()
|
target = target.lower()
|
||||||
if target.startswith('#'):
|
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)
|
return self.async_who_channel(channel=target, timeout=timeout)
|
||||||
else:
|
return None
|
||||||
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)
|
|
134
plugins/bomb.py
134
plugins/bomb.py
@ -1,46 +1,84 @@
|
|||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Periodic Messaging and Nickname Manipulation
|
||||||
|
|
||||||
|
This plugin for an IRC bot automates periodic messages, nickname changes,
|
||||||
|
and user listing within a channel. It supports starting and stopping these
|
||||||
|
tasks dynamically via bot commands.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Sends a periodic empty message (spam prevention technique).
|
||||||
|
- Changes the bot's nickname periodically to mimic users.
|
||||||
|
- Lists users in the channel periodically in manageable chunks.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import irc3
|
import irc3
|
||||||
import random
|
import random
|
||||||
from irc3.plugins.command import command
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class PeriodicMessagePlugin:
|
class PeriodicMessagePlugin:
|
||||||
|
"""A plugin to periodically send messages, change nicknames, and list users."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference and default parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.channel = ""
|
self.channel = "" # The IRC channel the bot is operating in
|
||||||
self.periodic_message = ' \u00A0 \u2002 \u2003 ' * 500
|
self.periodic_message = ' \u00A0 \u2002 \u2003 ' * 500 # Empty message trick
|
||||||
self.tasks = []
|
self.tasks = [] # Stores running asyncio tasks
|
||||||
self.running = False
|
self.running = False # Flag to control task execution
|
||||||
# Define sleep durations for each task
|
self.original_nick = "" # Store the original bot nickname
|
||||||
|
|
||||||
|
# Sleep durations for various tasks (in seconds)
|
||||||
self.sleep_durations = {
|
self.sleep_durations = {
|
||||||
'_send_periodic_message': 4,
|
'_send_periodic_message': 4,
|
||||||
'_change_nick_periodically': 50,
|
'_change_nick_periodically': 50,
|
||||||
'_send_listusers_periodically': 60
|
'_send_listusers_periodically': 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
@irc3.event(irc3.rfc.JOIN)
|
@irc3.event(irc3.rfc.JOIN)
|
||||||
def on_join(self, mask, channel, **kwargs):
|
def on_join(self, mask, channel, **kwargs):
|
||||||
"""Start periodic tasks when bot joins the channel."""
|
"""
|
||||||
|
Handle the bot joining a channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The user mask.
|
||||||
|
channel (str): The channel name.
|
||||||
|
kwargs (dict): Additional keyword arguments.
|
||||||
|
"""
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
if not self.running:
|
if not self.running:
|
||||||
pass
|
pass # Uncomment below if auto-starting periodic tasks
|
||||||
# self.start_periodic_tasks()
|
# self.start_periodic_tasks()
|
||||||
|
|
||||||
def start_periodic_tasks(self):
|
def start_periodic_tasks(self):
|
||||||
"""Start periodic messaging, nickname changing, and user listing tasks."""
|
"""Start all periodic tasks asynchronously."""
|
||||||
self.running = True
|
self.running = True
|
||||||
self._cancel_tasks()
|
self._cancel_tasks() # Ensure no duplicate tasks exist
|
||||||
self.tasks = [
|
self.tasks = [
|
||||||
asyncio.create_task(self._send_periodic_message()),
|
asyncio.create_task(self._send_periodic_message()),
|
||||||
asyncio.create_task(self._change_nick_periodically()),
|
asyncio.create_task(self._change_nick_periodically()),
|
||||||
asyncio.create_task(self._send_listusers_periodically())
|
asyncio.create_task(self._send_listusers_periodically()),
|
||||||
]
|
]
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
task.add_done_callback(self._handle_task_done)
|
task.add_done_callback(self._handle_task_done)
|
||||||
|
|
||||||
def _handle_task_done(self, task):
|
def _handle_task_done(self, task):
|
||||||
"""Handle task completion and restart if necessary."""
|
"""
|
||||||
|
Handle completed tasks and restart if necessary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task (asyncio.Task): The completed task.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
task.result()
|
task.result()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@ -49,14 +87,14 @@ class PeriodicMessagePlugin:
|
|||||||
self.bot.log.error(f"Task error: {e}")
|
self.bot.log.error(f"Task error: {e}")
|
||||||
finally:
|
finally:
|
||||||
if self.running:
|
if self.running:
|
||||||
self.start_periodic_tasks()
|
self.start_periodic_tasks() # Restart tasks if still running
|
||||||
|
|
||||||
async def _send_periodic_message(self):
|
async def _send_periodic_message(self):
|
||||||
"""Send a periodic message every X seconds defined by sleep_durations."""
|
"""Send an empty periodic message to the channel."""
|
||||||
try:
|
try:
|
||||||
while self.running:
|
while self.running:
|
||||||
self.bot.privmsg(self.channel, self.periodic_message)
|
self.bot.privmsg(self.channel, self.periodic_message)
|
||||||
self.bot.log.info(f"Message sent to {self.channel}: {self.periodic_message}")
|
self.bot.log.info(f"Message sent to {self.channel}.")
|
||||||
await asyncio.sleep(self.sleep_durations['_send_periodic_message'])
|
await asyncio.sleep(self.sleep_durations['_send_periodic_message'])
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
@ -64,24 +102,19 @@ class PeriodicMessagePlugin:
|
|||||||
self.bot.log.error(f"Error sending periodic message: {e}")
|
self.bot.log.error(f"Error sending periodic message: {e}")
|
||||||
|
|
||||||
async def _change_nick_periodically(self):
|
async def _change_nick_periodically(self):
|
||||||
"""Change nickname every X seconds to a random user's nickname with an underscore appended."""
|
"""Change the bot's nickname periodically to mimic a random user."""
|
||||||
try:
|
try:
|
||||||
self.original_nick = self.bot.nick
|
self.original_nick = self.bot.nick # Store original nickname
|
||||||
while self.running:
|
while self.running:
|
||||||
channel_key = self.channel.lower()
|
channel_key = self.channel.lower()
|
||||||
if channel_key in self.bot.channels:
|
if channel_key in self.bot.channels:
|
||||||
users = list(self.bot.channels[channel_key])
|
users = list(self.bot.channels[channel_key])
|
||||||
if users: # Ensure there are users in the channel to mimic
|
if users:
|
||||||
random_user = random.choice(users)
|
random_user = random.choice(users)
|
||||||
new_nick = f"{random_user}_"
|
new_nick = f"{random_user}_"
|
||||||
self.bot.send(f'NICK {new_nick}')
|
self.bot.send(f'NICK {new_nick}')
|
||||||
self.bot.log.info(f"Nickname changed to mimic: {random_user} as {new_nick}")
|
self.bot.nick = new_nick
|
||||||
if new_nick:
|
self.bot.log.info(f"Nickname changed to: {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'])
|
await asyncio.sleep(self.sleep_durations['_change_nick_periodically'])
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
@ -89,49 +122,50 @@ class PeriodicMessagePlugin:
|
|||||||
self.bot.log.error(f"Error changing nickname: {e}")
|
self.bot.log.error(f"Error changing nickname: {e}")
|
||||||
|
|
||||||
async def _send_listusers_periodically(self):
|
async def _send_listusers_periodically(self):
|
||||||
"""Send the list of users in the channel, truncating at spaces if over 100 characters."""
|
"""Send a list of users in the channel periodically."""
|
||||||
try:
|
try:
|
||||||
while self.running:
|
while self.running:
|
||||||
channel_key = self.channel.lower()
|
channel_key = self.channel.lower()
|
||||||
if channel_key in self.bot.channels:
|
if channel_key in self.bot.channels:
|
||||||
users = list(self.bot.channels[channel_key])
|
users = list(self.bot.channels[channel_key])
|
||||||
users_msg = ' '.join(users)
|
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)
|
chunks = textwrap.wrap(users_msg, width=400, break_long_words=False)
|
||||||
|
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
self.bot.privmsg(self.channel, chunk)
|
self.bot.privmsg(self.channel, chunk)
|
||||||
await asyncio.sleep(0.0001) # Small delay between chunks
|
await asyncio.sleep(0.0001) # Short delay to avoid flooding
|
||||||
self.bot.log.info(f"User list sent to {self.channel}.")
|
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'])
|
await asyncio.sleep(self.sleep_durations['_send_listusers_periodically'])
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bot.log.error(f"Error sending listusers periodically: {e}")
|
self.bot.log.error(f"Error sending user list: {e}")
|
||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
def stopannoy(self, mask, target, args):
|
def stopannoy(self, mask, target, args):
|
||||||
"""Stop all periodic tasks and revert the nickname back to the configured nick.
|
"""
|
||||||
|
Stop all periodic tasks and revert nickname.
|
||||||
%%stopannoy
|
|
||||||
|
Usage:
|
||||||
|
%%stopannoy
|
||||||
"""
|
"""
|
||||||
if mask.nick == self.bot.config.get('owner', ''):
|
if mask.nick == self.bot.config.get('owner', ''):
|
||||||
self.running = False
|
self.running = False
|
||||||
self._cancel_tasks()
|
self._cancel_tasks()
|
||||||
# Change nick back to the original configured nick
|
|
||||||
if self.original_nick:
|
if self.original_nick:
|
||||||
self.bot.send(f'NICK {self.original_nick}')
|
self.bot.send(f'NICK {self.original_nick}')
|
||||||
self.bot.nick = self.original_nick
|
self.bot.nick = self.original_nick
|
||||||
self.bot.log.info(f"Nickname reverted to: {self.original_nick}")
|
self.bot.log.info(f"Nickname reverted to: {self.original_nick}")
|
||||||
return "Periodic tasks stopped and nickname reverted."
|
return "Periodic tasks stopped."
|
||||||
return "Permission denied."
|
return "Permission denied."
|
||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def annoy(self, mask, target, args):
|
async def annoy(self, mask, target, args):
|
||||||
"""Start periodic tasks via the !startannoy command.
|
"""
|
||||||
|
Start periodic tasks via a command.
|
||||||
%%annoy
|
|
||||||
|
Usage:
|
||||||
|
%%annoy
|
||||||
"""
|
"""
|
||||||
if mask.nick == self.bot.config.get('owner', ''):
|
if mask.nick == self.bot.config.get('owner', ''):
|
||||||
if not self.running:
|
if not self.running:
|
||||||
@ -149,21 +183,21 @@ class PeriodicMessagePlugin:
|
|||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def listusers(self, mask, target, args):
|
async def listusers(self, mask, target, args):
|
||||||
"""List all users in the channel and send a message with the list in chunks.
|
"""
|
||||||
|
List all users in the channel and send a formatted message.
|
||||||
|
|
||||||
%%listusers
|
Usage:
|
||||||
|
%%listusers
|
||||||
"""
|
"""
|
||||||
self.channel = target
|
self.channel = target
|
||||||
channel_key = self.channel.lower()
|
channel_key = self.channel.lower()
|
||||||
if channel_key in self.bot.channels:
|
if channel_key in self.bot.channels:
|
||||||
users = list(self.bot.channels[channel_key])
|
users = list(self.bot.channels[channel_key])
|
||||||
chunk_size = 100 # Adjust chunk size as needed
|
chunk_size = 100
|
||||||
for i in range(0, len(users), chunk_size):
|
for i in range(0, len(users), chunk_size):
|
||||||
user_chunk = users[i:i + chunk_size]
|
user_chunk = users[i : i + chunk_size]
|
||||||
users_msg = ' '.join(user_chunk)
|
users_msg = ' '.join(user_chunk)
|
||||||
self.bot.privmsg(self.channel, f"{users_msg}")
|
self.bot.privmsg(self.channel, users_msg)
|
||||||
await asyncio.sleep(0.007) # Small delay between chunks
|
await asyncio.sleep(0.007) # Prevent flooding
|
||||||
return
|
|
||||||
#return f"List of users sent to {self.channel} in chunks."
|
|
||||||
else:
|
else:
|
||||||
return f"Channel {self.channel} not found."
|
return f"Channel {self.channel} not found."
|
@ -1,51 +1,109 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
========================================================
|
||||||
|
IRC3 Disregard Plugin (Flood-Based Message Suppression)
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
This plugin listens for messages from a **target user** and floods the
|
||||||
|
channel with **empty messages** to suppress visibility of their messages.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- **Admin-controlled** disregard system.
|
||||||
|
- **Flood-based suppression** (sends invisible characters).
|
||||||
|
- **Command to start/stop disregarding users**.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
!disregard <nick> -> Starts disregarding the specified user.
|
||||||
|
!stopdisregard -> Stops disregarding the current user.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
- The bot **detects messages** from the target nick and **floods** the chat.
|
||||||
|
- The bot **stops flooding** when the target is removed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class DisregardPlugin:
|
class DisregardPlugin:
|
||||||
def __init__(self, bot):
|
"""A plugin that disregards a user by flooding the chat with empty messages."""
|
||||||
|
|
||||||
|
def __init__(self, bot: irc3.IrcBot):
|
||||||
|
"""
|
||||||
|
Initialize the DisregardPlugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.target = None # The nick to disregard
|
self.target = None # The nick to disregard
|
||||||
self.flood_count = 25 # Number of empty messages to send
|
self.flood_count = 25 # Number of empty messages to send per detection
|
||||||
|
self.flood_message = '\u00A0\u2002\u2003' * 50 # Invisible characters
|
||||||
|
|
||||||
@irc3.event(irc3.rfc.PRIVMSG)
|
@irc3.event(irc3.rfc.PRIVMSG)
|
||||||
def on_privmsg(self, mask, event, target, data):
|
async def on_privmsg(self, mask, event, target, data):
|
||||||
"""
|
"""
|
||||||
Listens for messages and floods the channel with empty messages
|
Listens for messages and floods the channel if the sender is the target nick.
|
||||||
if the message is from the target nick.
|
|
||||||
|
|
||||||
:param mask: Contains info about the sender (mask.nick is the sender's nick)
|
Args:
|
||||||
:param target: The channel or user receiving the message
|
mask (str): Contains info about the sender (mask.nick is the sender's nick).
|
||||||
:param data: The text of the message
|
event (str): The IRC event type.
|
||||||
|
target (str): The channel or user receiving the message.
|
||||||
|
data (str): The message content.
|
||||||
"""
|
"""
|
||||||
if self.target and mask.nick == self.target:
|
if self.target and mask.nick.lower() == self.target.lower():
|
||||||
|
self.bot.log.info(f"Flooding {target} due to message from {self.target}.")
|
||||||
|
|
||||||
|
# Async flooding to avoid blocking
|
||||||
for _ in range(self.flood_count):
|
for _ in range(self.flood_count):
|
||||||
self.bot.privmsg(target, '\u00A0\u2002\u2003' * 50)
|
self.bot.privmsg(target, self.flood_message)
|
||||||
|
await asyncio.sleep(0.1) # Prevents immediate rate-limiting
|
||||||
|
|
||||||
@command(permission='admin', public=True)
|
@command(permission='admin', public=True)
|
||||||
def disregard(self, mask, target, args):
|
def disregard(self, mask, target, args):
|
||||||
"""
|
"""
|
||||||
Set the target nick to disregard.
|
Set a target nick to disregard (flood when they send messages).
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%disregard <nick>
|
%%disregard <nick>
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): Sender mask.
|
||||||
|
target (str): The channel where the command was sent.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Confirmation message.
|
||||||
"""
|
"""
|
||||||
user = args.get('<nick>')
|
user = args.get('<nick>')
|
||||||
if not user:
|
if not user:
|
||||||
self.bot.privmsg(target, "Usage: !disregard <nick>")
|
return "Usage: !disregard <nick>"
|
||||||
return
|
|
||||||
|
|
||||||
self.target = user
|
self.target = user.lower()
|
||||||
self.bot.privmsg(target, f"Now disregarding {user}. Their messages will trigger empty floods.")
|
self.bot.privmsg(target, f"Now disregarding {user}. Their messages will trigger empty floods.")
|
||||||
|
self.bot.log.info(f"Started disregarding {user}.")
|
||||||
|
|
||||||
@command(permission='admin', public=True)
|
@command(permission='admin', public=True)
|
||||||
def stopdisregard(self, mask, target, args):
|
def stopdisregard(self, mask, target, args):
|
||||||
"""
|
"""
|
||||||
Stop disregarding the current target.
|
Stop disregarding the current target nick.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%stopdisregard
|
%%stopdisregard
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): Sender mask.
|
||||||
|
target (str): The channel where the command was sent.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Confirmation message.
|
||||||
"""
|
"""
|
||||||
if self.target:
|
if self.target:
|
||||||
self.bot.privmsg(target, f"Stopped disregarding {self.target}.")
|
self.bot.privmsg(target, f"Stopped disregarding {self.target}.")
|
||||||
|
self.bot.log.info(f"Stopped disregarding {self.target}.")
|
||||||
self.target = None
|
self.target = None
|
||||||
else:
|
else:
|
||||||
self.bot.privmsg(target, "No target is currently being disregarded.")
|
self.bot.privmsg(target, "No target is currently being disregarded.")
|
||||||
|
@ -1,24 +1,71 @@
|
|||||||
|
"""
|
||||||
|
GoatPlugin.py
|
||||||
|
|
||||||
|
A plugin for the irc3 IRC bot framework that reads the contents of 'goat.txt' and sends each line
|
||||||
|
to a specified channel at regular intervals. If a task is already running for the target channel,
|
||||||
|
it notifies the user and prevents starting a new task.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
%%goat [<nick>]
|
||||||
|
%%goatstop
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
%%goat [<nick>]
|
||||||
|
Starts sending the contents of 'goat.txt' line by line to the target channel.
|
||||||
|
Optionally, specify a nickname to prepend to each message.
|
||||||
|
|
||||||
|
%%goatstop
|
||||||
|
Stops the ongoing goat task for the target channel.
|
||||||
|
|
||||||
|
Author: [Your Name]
|
||||||
|
Date Created: [Creation Date]
|
||||||
|
Last Modified: [Last Modification Date]
|
||||||
|
License: [License Information]
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class GoatPlugin:
|
class GoatPlugin:
|
||||||
|
"""
|
||||||
|
A plugin to send the contents of goat.txt line by line to a channel.
|
||||||
|
|
||||||
|
This plugin reads the contents of 'goat.txt' and sends each line to the target channel
|
||||||
|
at a regular interval. If a task is already running for the target channel, it will notify
|
||||||
|
the user and prevent starting a new task.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
goat_tasks (dict): A dictionary to keep track of running tasks for each target channel.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with the bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
# Dictionary to keep track of running tasks for each target (channel)
|
|
||||||
self.goat_tasks = {}
|
self.goat_tasks = {}
|
||||||
|
|
||||||
@command
|
@command
|
||||||
def goat(self, mask, target, args):
|
def goat(self, mask, target, args):
|
||||||
"""Send the contents of goat.txt line by line to the channel and resend when reaching the end.
|
"""
|
||||||
|
Send the contents of goat.txt line by line to the channel and resend when reaching the end.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The user mask.
|
||||||
|
target (str): The target channel or user.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%goat [<nick>]
|
%%goat [<nick>]
|
||||||
"""
|
"""
|
||||||
# Get the optional nick argument (it may be None or an empty string)
|
nick = args.get("<nick>")
|
||||||
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:
|
if target in self.goat_tasks:
|
||||||
self.bot.privmsg(target, "A goat task is already running.")
|
self.bot.privmsg(target, "A goat task is already running.")
|
||||||
return
|
return
|
||||||
@ -30,14 +77,20 @@ class GoatPlugin:
|
|||||||
self.bot.privmsg(target, f"Error reading goat.txt: {e}")
|
self.bot.privmsg(target, f"Error reading goat.txt: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Schedule sending the lines asynchronously and resend from the beginning.
|
|
||||||
task = self.bot.loop.create_task(self.send_lines(target, nick, lines))
|
task = self.bot.loop.create_task(self.send_lines(target, nick, lines))
|
||||||
self.goat_tasks[target] = task
|
self.goat_tasks[target] = task
|
||||||
|
|
||||||
@command
|
@command
|
||||||
def goatstop(self, mask, target, args):
|
def goatstop(self, mask, target, args):
|
||||||
"""Stop the goat command.
|
"""
|
||||||
|
Stop the goat command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The user mask.
|
||||||
|
target (str): The target channel or user.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%goatstop
|
%%goatstop
|
||||||
"""
|
"""
|
||||||
if target in self.goat_tasks:
|
if target in self.goat_tasks:
|
||||||
@ -48,23 +101,22 @@ class GoatPlugin:
|
|||||||
self.bot.privmsg(target, "No goat task is currently running.")
|
self.bot.privmsg(target, "No goat task is currently running.")
|
||||||
|
|
||||||
async def send_lines(self, target, nick, lines):
|
async def send_lines(self, target, nick, lines):
|
||||||
|
"""
|
||||||
|
Send lines of text to a target channel or user periodically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The target channel or user.
|
||||||
|
nick (str): Optional nickname to prepend to each message.
|
||||||
|
lines (list): List of lines to send.
|
||||||
|
"""
|
||||||
message_count = 0
|
message_count = 0
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped_line = line.strip()
|
stripped_line = line.strip()
|
||||||
# If nick is provided and non-empty, prepend it to the message.
|
msg = f"{nick} : {stripped_line}" if nick else stripped_line
|
||||||
if nick:
|
|
||||||
msg = f"{nick} : {stripped_line}"
|
|
||||||
else:
|
|
||||||
msg = stripped_line
|
|
||||||
self.bot.privmsg(target, msg)
|
self.bot.privmsg(target, msg)
|
||||||
message_count += 1
|
message_count += 1
|
||||||
|
|
||||||
# Optional: add periodic delays if needed.
|
|
||||||
# if message_count % 1000 == 0:
|
|
||||||
# await asyncio.sleep(5)
|
|
||||||
|
|
||||||
await asyncio.sleep(0.007)
|
await asyncio.sleep(0.007)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.bot.privmsg(target, "Goat task cancelled.")
|
self.bot.privmsg(target, "Goat task cancelled.")
|
||||||
|
@ -1,3 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Imitator
|
||||||
|
|
||||||
|
This plugin for an IRC bot allows the bot to imitate another user by repeating
|
||||||
|
messages sent by the target user. It also supports an optional Unicode glitch
|
||||||
|
styling mode for the repeated messages.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Imitates messages from a specified user.
|
||||||
|
- Optionally applies Unicode glitch styling to the messages.
|
||||||
|
- Supports starting and stopping the imitation via bot commands.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
=====
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def imitate(self, mask, target, args):
|
||||||
|
%%imitate [--stop] [--unicode] [<nick>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--stop Stop imitating.
|
||||||
|
--unicode Enable Unicode glitch styling.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
"""
|
||||||
|
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
import random
|
import random
|
||||||
@ -25,7 +54,15 @@ COMBINING_CHARS = [
|
|||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class Imitator:
|
class Imitator:
|
||||||
|
"""A plugin to imitate another user by repeating their messages."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.target = None
|
self.target = None
|
||||||
self.unicode_mode = False # Flag to enable Unicode glitch styling
|
self.unicode_mode = False # Flag to enable Unicode glitch styling
|
||||||
|
@ -1,16 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Matrix-style Character Rain
|
||||||
|
|
||||||
|
This plugin for an IRC bot generates and displays a Matrix-style rain of characters
|
||||||
|
in a specified channel. The characters are randomly selected and colorized to
|
||||||
|
create the visual effect.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Generates a specified number of lines of random characters.
|
||||||
|
- Colorizes each character with a random IRC color.
|
||||||
|
- Sends the generated lines to the target channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def matrix(self, mask, target, args):
|
||||||
|
%%matrix
|
||||||
|
|
||||||
|
Author: kevinpostal
|
||||||
|
Date: 2025-02-13 06:12:10 (UTC)
|
||||||
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
CHAR_LIST = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
|
# List of characters to be used in the Matrix-style rain
|
||||||
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
CHAR_LIST = [
|
||||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
"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",
|
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "!", "#", "$",
|
"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", "!", "#", "$",
|
||||||
"\\", "/", '"']
|
"%", "^", "&", "(", ")", "-", "+", "=", "[", "]", "{", "}", "|",
|
||||||
|
";", ":", "<", ">", ",", ".", "?", "~", "`", "@", "*", "_", "'",
|
||||||
|
"\\", "/", '"'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Dictionary of IRC color codes
|
||||||
IRC_COLORS = {
|
IRC_COLORS = {
|
||||||
'white': '\x0300', 'black': '\x0301', 'blue': '\x0302', 'green': '\x0303',
|
'white': '\x0300', 'black': '\x0301', 'blue': '\x0302', 'green': '\x0303',
|
||||||
'red': '\x0304', 'brown': '\x0305', 'purple': '\x0306', 'orange': '\x0307',
|
'red': '\x0304', 'brown': '\x0305', 'purple': '\x0306', 'orange': '\x0307',
|
||||||
@ -20,7 +50,15 @@ IRC_COLORS = {
|
|||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class MatrixPlugin:
|
class MatrixPlugin:
|
||||||
|
"""A plugin to display a Matrix-style rain of characters in a channel."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@command
|
@command
|
||||||
@ -28,13 +66,29 @@ class MatrixPlugin:
|
|||||||
"""
|
"""
|
||||||
Display a Matrix-style rain of characters.
|
Display a Matrix-style rain of characters.
|
||||||
|
|
||||||
%%matrix
|
Args:
|
||||||
|
mask (str): The user mask.
|
||||||
|
target (str): The target channel or user.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
%%matrix
|
||||||
"""
|
"""
|
||||||
matrix_lines = self.generate_matrix_lines(20, 80)
|
matrix_lines = self.generate_matrix_lines(20, 80)
|
||||||
for line in matrix_lines:
|
for line in matrix_lines:
|
||||||
self.bot.privmsg(target, line)
|
self.bot.privmsg(target, line)
|
||||||
|
|
||||||
def generate_matrix_lines(self, lines, length):
|
def generate_matrix_lines(self, lines, length):
|
||||||
|
"""
|
||||||
|
Generate lines of Matrix-style rain characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lines (int): Number of lines to generate.
|
||||||
|
length (int): Length of each line.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of generated lines.
|
||||||
|
"""
|
||||||
matrix_lines = []
|
matrix_lines = []
|
||||||
for _ in range(lines):
|
for _ in range(lines):
|
||||||
line = ''.join(random.choice(CHAR_LIST) for _ in range(length))
|
line = ''.join(random.choice(CHAR_LIST) for _ in range(length))
|
||||||
@ -43,6 +97,15 @@ class MatrixPlugin:
|
|||||||
return matrix_lines
|
return matrix_lines
|
||||||
|
|
||||||
def colorize(self, text):
|
def colorize(self, text):
|
||||||
|
"""
|
||||||
|
Apply random IRC colors to each character in the text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The text to colorize.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Colorized text.
|
||||||
|
"""
|
||||||
colored_text = ""
|
colored_text = ""
|
||||||
for char in text:
|
for char in text:
|
||||||
color = random.choice(list(IRC_COLORS.values()))
|
color = random.choice(list(IRC_COLORS.values()))
|
||||||
|
@ -1,12 +1,39 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: YouTube Video Information Fetcher
|
||||||
|
|
||||||
from irc3.plugins.command import command
|
This plugin for an IRC bot fetches and displays YouTube video information. It responds to both command inputs and messages containing YouTube links.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Fetches video details like title, duration, views, likes, and comments.
|
||||||
|
- Parses and formats YouTube video durations.
|
||||||
|
- Formats numbers for readability using K for thousands and M for millions.
|
||||||
|
- Responds to YouTube links in messages.
|
||||||
|
- Provides a command to search for YouTube videos.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
=====
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
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.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-13 06:13:59 (UTC)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
import irc3
|
import irc3
|
||||||
import html
|
import html
|
||||||
import googleapiclient.discovery
|
import googleapiclient.discovery
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
import shlex
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
# Constants for YouTube API
|
# Constants for YouTube API
|
||||||
API_SERVICE_NAME = "youtube"
|
API_SERVICE_NAME = "youtube"
|
||||||
|
@ -1,24 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Mass Messaging
|
||||||
|
|
||||||
|
This plugin for an IRC bot enables mass messaging to all users in a channel using an asynchronous queue system.
|
||||||
|
It supports sending messages to multiple users with a small delay between messages to prevent flooding.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Sends a message to all users in a channel.
|
||||||
|
- Uses an asynchronous queue system for efficient message processing.
|
||||||
|
- Handles IRC mode prefixes in nicknames.
|
||||||
|
- Provides logging for sent and failed messages.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def msgall(self, mask, target, args):
|
||||||
|
%%msgall <message>...
|
||||||
|
|
||||||
|
This command can only be used in a channel.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-13 06:15:38 (UTC)
|
||||||
|
"""
|
||||||
|
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
from irc3.compat import Queue
|
from irc3.compat import Queue
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
def strip_nick_prefix(nick):
|
def strip_nick_prefix(nick):
|
||||||
"""Remove IRC mode prefixes from a nickname"""
|
"""Remove IRC mode prefixes from a nickname."""
|
||||||
return nick.lstrip('@+%&~!') if nick else ''
|
return nick.lstrip('@+%&~!') if nick else ''
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class MassMessagePlugin:
|
class MassMessagePlugin:
|
||||||
"""Mass messaging plugin using async queue system"""
|
"""Mass messaging plugin using async queue system."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.delay = 0.0001 # Delay between messages in seconds
|
self.delay = 0.0001 # Delay between messages in seconds
|
||||||
self.queue = Queue() # Using irc3's compatibility Queue
|
self.queue = Queue() # Using irc3's compatibility Queue
|
||||||
self.count = 0 # Counter for successfully sent messages
|
self.count = 0 # Counter for successfully sent messages
|
||||||
|
|
||||||
async def _worker(self, message, total):
|
async def _worker(self, message, total):
|
||||||
"""Worker task to process messages from the queue"""
|
"""
|
||||||
|
Worker task to process messages from the queue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): The message to send.
|
||||||
|
total (int): The total number of recipients.
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
nick = await self.queue.get()
|
nick = await self.queue.get()
|
||||||
@ -45,12 +87,15 @@ class MassMessagePlugin:
|
|||||||
|
|
||||||
@command(permission="admin", options_first=True)
|
@command(permission="admin", options_first=True)
|
||||||
async def msgall(self, mask, target, args):
|
async def msgall(self, mask, target, args):
|
||||||
"""Send a message to all users in the channel using async queue
|
"""
|
||||||
|
Send a message to all users in the channel using async queue.
|
||||||
|
|
||||||
%%msgall <message>...
|
%%msgall <message>...
|
||||||
|
|
||||||
|
This command can only be used in a channel.
|
||||||
"""
|
"""
|
||||||
if not target.is_channel:
|
if not target.is_channel:
|
||||||
return "This command can only be used in a channel"
|
return "This command can only be used in a channel."
|
||||||
|
|
||||||
message = ' '.join(args['<message>'])
|
message = ' '.join(args['<message>'])
|
||||||
workers = [] # Ensure workers is defined in the scope of the try block
|
workers = [] # Ensure workers is defined in the scope of the try block
|
||||||
@ -62,7 +107,7 @@ class MassMessagePlugin:
|
|||||||
recipients = [n for n in nicknames if n != self.bot.nick]
|
recipients = [n for n in nicknames if n != self.bot.nick]
|
||||||
|
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return "No valid recipients found"
|
return "No valid recipients found."
|
||||||
|
|
||||||
total = len(recipients)
|
total = len(recipients)
|
||||||
self.count = 0 # Reset the counter for this run
|
self.count = 0 # Reset the counter for this run
|
||||||
|
@ -1,16 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC Bot Plugins: Voice, Kick, and Ban Management
|
||||||
|
|
||||||
|
This module provides three IRC bot plugins:
|
||||||
|
1. `VoicePlugin`: Handles granting and revoking voice (+v) privileges.
|
||||||
|
2. `KickPlugin`: Handles kicking users from the channel.
|
||||||
|
3. `BanPlugin`: Handles banning and unbanning users.
|
||||||
|
|
||||||
|
All commands require **admin** permissions.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Voice a single user or all users in a channel.
|
||||||
|
- Devoice a single user or all users.
|
||||||
|
- Kick users with optional reasons.
|
||||||
|
- Ban and unban users.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import irc3
|
import irc3
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
import asyncio
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class VoicePlugin:
|
class VoicePlugin:
|
||||||
|
"""A plugin to manage voice (+v) privileges in an IRC channel."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the VoicePlugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def voice(self, mask, target, args):
|
async def voice(self, mask, target, args):
|
||||||
"""Give voice to all users or a specific user
|
"""
|
||||||
|
Grant voice to a specific user or all users in the channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%voice [<nick>]
|
%%voice [<nick>]
|
||||||
"""
|
"""
|
||||||
nick = args.get('<nick>')
|
nick = args.get('<nick>')
|
||||||
@ -21,8 +52,10 @@ class VoicePlugin:
|
|||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def devoice(self, mask, target, args):
|
async def devoice(self, mask, target, args):
|
||||||
"""Remove voice from all users or a specific user
|
"""
|
||||||
|
Remove voice from a specific user or all users in the channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%devoice [<nick>]
|
%%devoice [<nick>]
|
||||||
"""
|
"""
|
||||||
nick = args.get('<nick>')
|
nick = args.get('<nick>')
|
||||||
@ -32,38 +65,71 @@ class VoicePlugin:
|
|||||||
await self.remove_voice_all(target)
|
await self.remove_voice_all(target)
|
||||||
|
|
||||||
async def give_voice(self, target, nick):
|
async def give_voice(self, target, nick):
|
||||||
"""Give voice to a specific user"""
|
"""
|
||||||
|
Grant voice to a specific user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
nick (str): The nickname of the user.
|
||||||
|
"""
|
||||||
self.bot.send(f'MODE {target} +v {nick}')
|
self.bot.send(f'MODE {target} +v {nick}')
|
||||||
|
|
||||||
async def remove_voice(self, target, nick):
|
async def remove_voice(self, target, nick):
|
||||||
"""Remove voice from a specific user"""
|
"""
|
||||||
|
Remove voice from a specific user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
nick (str): The nickname of the user.
|
||||||
|
"""
|
||||||
self.bot.send(f'MODE {target} -v {nick}')
|
self.bot.send(f'MODE {target} -v {nick}')
|
||||||
|
|
||||||
async def give_voice_all(self, target):
|
async def give_voice_all(self, target):
|
||||||
"""Give voice to all users in the channel who currently don't have it"""
|
"""
|
||||||
|
Grant voice to all users in the channel who do not have it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
"""
|
||||||
names = await self.bot.async_cmds.names(target)
|
names = await self.bot.async_cmds.names(target)
|
||||||
for user in names['names']:
|
for user in names['names']:
|
||||||
if not user.startswith("+") and not user.startswith("@"):
|
if not user.startswith(("+", "@")): # Ignore voiced/opped users
|
||||||
self.bot.send(f'MODE {target} +v {user}')
|
self.bot.send(f'MODE {target} +v {user}')
|
||||||
await asyncio.sleep(0.07) # To avoid flooding the server with commands
|
await asyncio.sleep(0.07) # Prevent server flooding
|
||||||
|
|
||||||
async def remove_voice_all(self, target):
|
async def remove_voice_all(self, target):
|
||||||
"""Remove voice from all users in the channel"""
|
"""
|
||||||
|
Remove voice from all users in the channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
"""
|
||||||
names = await self.bot.async_cmds.names(target)
|
names = await self.bot.async_cmds.names(target)
|
||||||
for user in names['names']:
|
for user in names['names']:
|
||||||
if user.startswith("+"):
|
if user.startswith("+"): # Only devoice voiced users
|
||||||
self.bot.send(f'MODE {target} -v {user.lstrip("+")}')
|
self.bot.send(f'MODE {target} -v {user.lstrip("+")}')
|
||||||
await asyncio.sleep(0.07) # To avoid flooding the server with commands
|
await asyncio.sleep(0.07) # Prevent server flooding
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class KickPlugin:
|
class KickPlugin:
|
||||||
|
"""A plugin to kick users from an IRC channel."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the KickPlugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def kick(self, mask, target, args):
|
async def kick(self, mask, target, args):
|
||||||
"""Kick a specific user from the channel
|
"""
|
||||||
|
Kick a specific user from the channel with an optional reason.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%kick <nick> [<reason>]
|
%%kick <nick> [<reason>]
|
||||||
"""
|
"""
|
||||||
nick = args.get('<nick>')
|
nick = args.get('<nick>')
|
||||||
@ -72,18 +138,36 @@ class KickPlugin:
|
|||||||
await self.kick_user(target, nick, reason)
|
await self.kick_user(target, nick, reason)
|
||||||
|
|
||||||
async def kick_user(self, target, nick, reason):
|
async def kick_user(self, target, nick, reason):
|
||||||
"""Kick a specific user from the channel using ChanServ"""
|
"""
|
||||||
|
Kick a user from the channel using ChanServ.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
nick (str): The nickname of the user.
|
||||||
|
reason (str): The reason for kicking the user.
|
||||||
|
"""
|
||||||
self.bot.send(f'PRIVMSG ChanServ :KICK {target} {nick} {reason}')
|
self.bot.send(f'PRIVMSG ChanServ :KICK {target} {nick} {reason}')
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class BanPlugin:
|
class BanPlugin:
|
||||||
|
"""A plugin to ban and unban users in an IRC channel."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the BanPlugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def ban(self, mask, target, args):
|
async def ban(self, mask, target, args):
|
||||||
"""Ban a specific user from the channel
|
"""
|
||||||
|
Ban a specific user from the channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%ban <nick>
|
%%ban <nick>
|
||||||
"""
|
"""
|
||||||
nick = args.get('<nick>')
|
nick = args.get('<nick>')
|
||||||
@ -92,8 +176,10 @@ class BanPlugin:
|
|||||||
|
|
||||||
@command(permission='admin')
|
@command(permission='admin')
|
||||||
async def unban(self, mask, target, args):
|
async def unban(self, mask, target, args):
|
||||||
"""Unban a specific user from the channel
|
"""
|
||||||
|
Unban a specific user from the channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%unban <nick>
|
%%unban <nick>
|
||||||
"""
|
"""
|
||||||
nick = args.get('<nick>')
|
nick = args.get('<nick>')
|
||||||
@ -101,9 +187,21 @@ class BanPlugin:
|
|||||||
await self.unban_user(target, nick)
|
await self.unban_user(target, nick)
|
||||||
|
|
||||||
async def ban_user(self, target, nick):
|
async def ban_user(self, target, nick):
|
||||||
"""Ban a specific user from the channel"""
|
"""
|
||||||
|
Ban a specific user from the channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
nick (str): The nickname of the user.
|
||||||
|
"""
|
||||||
self.bot.send(f'MODE {target} +b {nick}')
|
self.bot.send(f'MODE {target} +b {nick}')
|
||||||
|
|
||||||
async def unban_user(self, target, nick):
|
async def unban_user(self, target, nick):
|
||||||
"""Unban a specific user from the channel"""
|
"""
|
||||||
self.bot.send(f'MODE {target} -b {nick}')
|
Unban a specific user from the channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target (str): The IRC channel.
|
||||||
|
nick (str): The nickname of the user.
|
||||||
|
"""
|
||||||
|
self.bot.send(f'MODE {target} -b {nick}')
|
||||||
|
@ -1,34 +1,75 @@
|
|||||||
from irc3.plugins.command import command
|
# -*- coding: utf-8 -*-
|
||||||
from irc3.plugins.cron import cron
|
"""
|
||||||
import irc3
|
IRC3 Anti-Spam Plugin with Auto-Kick and Auto-Ban
|
||||||
from irc3 import utils
|
|
||||||
import re
|
This plugin automatically detects and mitigates spam in IRC channels
|
||||||
from collections import defaultdict, deque
|
by monitoring messages for:
|
||||||
|
- Excessive message repetition
|
||||||
|
- High-frequency messaging (flooding)
|
||||||
|
- Excessive bot mentions
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
- **1st and 2nd offense**: User gets kicked.
|
||||||
|
- **3rd offense within 5 minutes**: User gets banned.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Uses a dynamic WHO query to verify user modes before action.
|
||||||
|
- Configurable limits for spam detection (via `antispam` settings).
|
||||||
|
- Periodic cleanup of inactive users every minute.
|
||||||
|
|
||||||
|
Author: [Your Name]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from asynchronous import AsyncEvents
|
|
||||||
|
import irc3
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from irc3.plugins.cron import cron
|
||||||
|
from asynchronous import WhoChannel
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class AntiSpam:
|
class AntiSpam:
|
||||||
"""IRC3 Anti-Spam Plugin with Auto-Ban for Repeat Offenders"""
|
"""A plugin for automatic spam detection and mitigation in IRC channels."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot: irc3.IrcBot):
|
||||||
|
"""
|
||||||
|
Initialize the AntiSpam plugin with configurable thresholds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = bot.config.get('antispam', {})
|
self.config = bot.config.get('antispam', {})
|
||||||
|
|
||||||
|
# User activity tracking
|
||||||
self.user_data = defaultdict(lambda: {
|
self.user_data = defaultdict(lambda: {
|
||||||
'messages': deque(maxlen=int(self.config.get('repeat_limit', 3))),
|
'messages': deque(maxlen=int(self.config.get('repeat_limit', 3))),
|
||||||
'timestamps': deque(maxlen=int(self.config.get('spam_limit', 5))),
|
'timestamps': deque(maxlen=int(self.config.get('spam_limit', 5))),
|
||||||
'mentions': deque(maxlen=int(self.config.get('mention_limit', 2)))
|
'mentions': deque(maxlen=int(self.config.get('mention_limit', 2)))
|
||||||
})
|
})
|
||||||
self.kick_history = defaultdict(deque) # Track kick timestamps per user
|
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):
|
self.exclude_list = ['ZodBot'] # Bots that should be ignored
|
||||||
"""Dynamically fetch user modes using WHO command."""
|
self.service_name = self.config.get('service_name', 'ChanServ')
|
||||||
|
|
||||||
|
self.who_channel = WhoChannel(bot) # WHO query for user modes
|
||||||
|
|
||||||
|
async def get_user_modes(self, nick: str, channel: str) -> str:
|
||||||
|
"""
|
||||||
|
Retrieve user modes dynamically using the WHO command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nick (str): The user's nickname.
|
||||||
|
channel (str): The IRC channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: User's mode (e.g., 'o' for op, 'v' for voice, etc.).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
result = await self.who_channel(channel=channel)
|
result = await self.who_channel(channel=channel)
|
||||||
if result['success']:
|
if result.get('success'):
|
||||||
for user in result['users']:
|
for user in result['users']:
|
||||||
if user['nick'].lower() == nick.lower():
|
if user['nick'].lower() == nick.lower():
|
||||||
return user['modes']
|
return user['modes']
|
||||||
@ -36,36 +77,59 @@ class AntiSpam:
|
|||||||
self.bot.log.error(f"Error fetching user modes: {e}")
|
self.bot.log.error(f"Error fetching user modes: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def is_spam(self, nick, message, channel):
|
def is_spam(self, nick: str, message: str, channel: str) -> bool:
|
||||||
"""Check if message meets spam criteria"""
|
"""
|
||||||
|
Determine whether a message meets spam criteria.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nick (str): The user's nickname.
|
||||||
|
message (str): The message content.
|
||||||
|
channel (str): The channel where the message was sent.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the message is considered spam, False otherwise.
|
||||||
|
"""
|
||||||
user = self.user_data[nick.lower()]
|
user = self.user_data[nick.lower()]
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
|
# Check message length
|
||||||
if len(message) > int(self.config.get('max_length', 300)):
|
if len(message) > int(self.config.get('max_length', 300)):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check repeated messages
|
||||||
if message in user['messages']:
|
if message in user['messages']:
|
||||||
if len(user['messages']) == user['messages'].maxlen - 1:
|
if len(user['messages']) == user['messages'].maxlen - 1:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check rapid message spam (flooding)
|
||||||
user['timestamps'].append(now)
|
user['timestamps'].append(now)
|
||||||
if len(user['timestamps']) == user['timestamps'].maxlen:
|
if len(user['timestamps']) == user['timestamps'].maxlen:
|
||||||
if (now - user['timestamps'][0]) < 60:
|
if (now - user['timestamps'][0]) < 60:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check excessive bot mentions
|
||||||
if self.bot.nick.lower() in message.lower():
|
if self.bot.nick.lower() in message.lower():
|
||||||
user['mentions'].append(now)
|
user['mentions'].append(now)
|
||||||
if len(user['mentions']) == user['mentions'].maxlen:
|
if len(user['mentions']) == user['mentions'].maxlen:
|
||||||
if (now - user['mentions'][0]) < 60:
|
if (now - user['mentions'][0]) < 60:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Store message to check for repetition
|
||||||
user['messages'].append(message)
|
user['messages'].append(message)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@irc3.event(irc3.rfc.PRIVMSG)
|
@irc3.event(irc3.rfc.PRIVMSG)
|
||||||
async def monitor_messages(self, mask, event, target, data):
|
async def monitor_messages(self, mask, event, target, data):
|
||||||
"""Handle incoming messages and check for spam"""
|
"""
|
||||||
if target.startswith("#"):
|
Monitor incoming messages for spam and take action if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The sender's mask.
|
||||||
|
event (str): The IRC event type.
|
||||||
|
target (str): The channel where the message was sent.
|
||||||
|
data (str): The message content.
|
||||||
|
"""
|
||||||
|
if target.startswith("#"): # Process only channel messages
|
||||||
nick = mask.nick
|
nick = mask.nick
|
||||||
message = data
|
message = data
|
||||||
channel_name = target.lower()
|
channel_name = target.lower()
|
||||||
@ -73,95 +137,69 @@ class AntiSpam:
|
|||||||
if nick in self.exclude_list:
|
if nick in self.exclude_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Fetch user modes to avoid acting on moderators
|
||||||
user_modes = await self.get_user_modes(nick, channel_name)
|
user_modes = await self.get_user_modes(nick, channel_name)
|
||||||
|
if user_modes and {'o', '%', 'h', '@'} & set(user_modes):
|
||||||
if user_modes:
|
return
|
||||||
if {'o', '%', 'h', '@'} & set(user_modes):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.is_spam(nick, message, channel_name):
|
if self.is_spam(nick, message, channel_name):
|
||||||
print(f"SPAM {nick} - {user_modes}")
|
self.bot.log.info(f"SPAM detected from {nick}: {message}")
|
||||||
self.handle_spam(mask, message, channel_name)
|
self.handle_spam(mask, message, channel_name)
|
||||||
|
|
||||||
def handle_spam(self, mask, message, channel):
|
def handle_spam(self, mask, message, channel):
|
||||||
"""Take action against spam, escalating to ban if kicked twice in 5 minutes"""
|
"""
|
||||||
|
Take action against spamming users, escalating to ban if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The sender's mask.
|
||||||
|
message (str): The spam message.
|
||||||
|
channel (str): The IRC channel.
|
||||||
|
"""
|
||||||
nick = mask.nick
|
nick = mask.nick
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
cutoff = current_time - 300 # 5 minutes ago
|
cutoff = current_time - 300 # 5-minute window
|
||||||
|
|
||||||
nick_lower = nick.lower()
|
user_kicks = self.kick_history[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]
|
recent_kicks = [ts for ts in user_kicks if ts >= cutoff]
|
||||||
|
|
||||||
if len(recent_kicks) >= 2:
|
if len(recent_kicks) >= 2:
|
||||||
# Ban the user using hostmask
|
# Ban the user
|
||||||
ban_mask = f'*!{mask.host}'
|
ban_mask = f'*!{mask.host}'
|
||||||
|
|
||||||
self.bot.send(f"MODE {channel} +b {ban_mask}")
|
self.bot.send(f"MODE {channel} +b {ban_mask}")
|
||||||
|
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
self.service_name,
|
self.service_name,
|
||||||
f"KICK {channel} {nick} Banned for repeated spamming"
|
f"KICK {channel} {nick} :Banned for repeated spamming"
|
||||||
)
|
)
|
||||||
# Clear history and data
|
|
||||||
del self.kick_history[nick_lower]
|
self.bot.log.info(f"{nick} banned for repeated spamming.")
|
||||||
self.user_data.pop(nick_lower, None)
|
|
||||||
|
# Clear data
|
||||||
|
del self.kick_history[nick.lower()]
|
||||||
|
self.user_data.pop(nick.lower(), None)
|
||||||
else:
|
else:
|
||||||
# Kick and record timestamp
|
# Kick the user and log action
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
self.service_name,
|
self.service_name,
|
||||||
f"KICK {channel} {nick} :stop spamming"
|
f"KICK {channel} {nick} :Stop spamming."
|
||||||
)
|
)
|
||||||
user_kicks.append(current_time)
|
user_kicks.append(current_time)
|
||||||
self.user_data.pop(nick_lower, None)
|
self.bot.log.info(f"{nick} kicked for spam. Warning count: {len(recent_kicks) + 1}")
|
||||||
|
|
||||||
|
# Clear message history for the user
|
||||||
|
self.user_data.pop(nick.lower(), None)
|
||||||
|
|
||||||
@cron('* * * * *')
|
@cron('* * * * *')
|
||||||
def clean_old_records(self):
|
def clean_old_records(self):
|
||||||
"""Cleanup inactive users every minute"""
|
"""Clean up inactive user records every minute."""
|
||||||
cutoff = time.time() - 300
|
cutoff = time.time() - 300
|
||||||
to_remove = [
|
self.user_data = {
|
||||||
nick for nick, data in self.user_data.items()
|
nick: data
|
||||||
if len(data['timestamps']) > 0 and data['timestamps'][-1] < cutoff
|
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]
|
self.bot.log.info("Cleaned up old spam records.")
|
||||||
|
|
||||||
def connection_made(self):
|
def connection_made(self):
|
||||||
"""Initialize when bot connects"""
|
"""Initialize when bot connects."""
|
||||||
self.bot.log.info("Enhanced AntiSpam plugin loaded with kick-to-ban escalation")
|
self.bot.log.info("AntiSpam plugin loaded with automated 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
|
|
||||||
|
@ -1,16 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Say Command with Text Styling
|
||||||
|
|
||||||
|
This plugin for an IRC bot allows the bot to send styled messages to a specified channel using the say command.
|
||||||
|
The messages are styled with random IRC color codes, bold, and underline, along with Unicode combining characters.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Sends a styled message to a specified channel.
|
||||||
|
- Applies random IRC colors, bold, and underline styles to the message.
|
||||||
|
- Uses Unicode combining characters to create a glitch effect.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def say(self, mask, target, args):
|
||||||
|
%%say <channel> <message>...
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-13 06:24:46 (UTC)
|
||||||
|
"""
|
||||||
|
|
||||||
import irc3
|
import irc3
|
||||||
import random
|
import random
|
||||||
from irc3.plugins.command import command
|
from irc3.plugins.command import command
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class SayPlugin:
|
class SayPlugin:
|
||||||
|
"""A plugin to send styled messages to a specified channel using the say command."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@command
|
@command
|
||||||
def say(self, mask, target, args):
|
def say(self, mask, target, args):
|
||||||
"""Say command
|
"""
|
||||||
|
Say command to send a styled message to a specified channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The user mask.
|
||||||
|
target (str): The target channel or user.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%say <channel> <message>...
|
%%say <channel> <message>...
|
||||||
"""
|
"""
|
||||||
channel = args.get('<channel>')
|
channel = args.get('<channel>')
|
||||||
@ -24,6 +64,15 @@ class SayPlugin:
|
|||||||
self.bot.privmsg(channel, styled_message)
|
self.bot.privmsg(channel, styled_message)
|
||||||
|
|
||||||
def add_combining_characters(self, char):
|
def add_combining_characters(self, char):
|
||||||
|
"""
|
||||||
|
Add random combining characters (with style and color codes) to a character.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
char (str): The character to style.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The styled character.
|
||||||
|
"""
|
||||||
combining_chars = [
|
combining_chars = [
|
||||||
'\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305',
|
'\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305',
|
||||||
'\u0306', '\u0307', '\u0308', '\u0309', '\u030A', '\u030B',
|
'\u0306', '\u0307', '\u0308', '\u0309', '\u030A', '\u030B',
|
||||||
@ -52,6 +101,15 @@ class SayPlugin:
|
|||||||
return glitched_char
|
return glitched_char
|
||||||
|
|
||||||
def style_message(self, message):
|
def style_message(self, message):
|
||||||
|
"""
|
||||||
|
Apply styling to each character in the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): The message to style.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The styled message.
|
||||||
|
"""
|
||||||
white_color_code = '\x0300' # White color
|
white_color_code = '\x0300' # White color
|
||||||
styled_message = ''
|
styled_message = ''
|
||||||
|
|
||||||
|
@ -1,3 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
IRC3 Bot Plugin: Unicode Spammer and Nickname Changer
|
||||||
|
|
||||||
|
This plugin for an IRC bot enables continuous spamming of Unicode characters to a specified target and periodically changes the bot's nickname to mimic a random user in the channel.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Spams messages composed of valid Unicode characters to a specified target.
|
||||||
|
- Periodically changes the bot's nickname to mimic a random user in the channel.
|
||||||
|
- Handles starting and stopping of spam and nickname-changing tasks via bot commands.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def unicode(self, mask, target, args):
|
||||||
|
%%unicode [<target>]
|
||||||
|
|
||||||
|
@command
|
||||||
|
def unicodestop(self, mask, target, args):
|
||||||
|
%%unicodestop [<target>]
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-13 06:25:41 (UTC)
|
||||||
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import irc3
|
import irc3
|
||||||
@ -5,9 +33,18 @@ from irc3.plugins.command import command
|
|||||||
import asyncio
|
import asyncio
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class UnicodeSpammer:
|
class UnicodeSpammer:
|
||||||
|
"""A plugin to spam Unicode characters and change the bot's nickname periodically."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.valid_code_points = self._generate_valid_code_points()
|
self.valid_code_points = self._generate_valid_code_points()
|
||||||
random.shuffle(self.valid_code_points)
|
random.shuffle(self.valid_code_points)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
IRC Bot Plugin for Uploading Files to hardfiles.org
|
IRC Bot Plugin for Uploading Files to hardfiles.org
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ Dependencies:
|
|||||||
- yt-dlp
|
- yt-dlp
|
||||||
- ircstyle
|
- ircstyle
|
||||||
|
|
||||||
Author: Your Name
|
Author: Zodiac
|
||||||
Version: 1.2
|
Version: 1.2
|
||||||
Date: 2025-02-12
|
Date: 2025-02-12
|
||||||
"""
|
"""
|
||||||
@ -36,16 +37,26 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class UploadPlugin:
|
class UploadPlugin:
|
||||||
"""
|
"""IRC bot plugin for downloading files via yt-dlp and uploading them to hardfiles.org."""
|
||||||
IRC bot plugin for downloading files via yt-dlp and uploading them to hardfiles.org.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the UploadPlugin with an IRC bot instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
def _ensure_str(self, value):
|
def _ensure_str(self, value):
|
||||||
"""
|
"""
|
||||||
Ensure the value is a string. If it's bytes, decode it as UTF-8 with error replacement.
|
Ensure the value is a string. If it's bytes, decode it as UTF-8 with error replacement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Union[str, bytes, None]): The value to ensure as a string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The value as a string.
|
||||||
"""
|
"""
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
return value.decode('utf-8', errors='replace')
|
return value.decode('utf-8', errors='replace')
|
||||||
@ -59,9 +70,9 @@ class UploadPlugin:
|
|||||||
Upload a file to hardfiles.org (Max 100MB).
|
Upload a file to hardfiles.org (Max 100MB).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mask: The user mask (nickname@host) of the command issuer.
|
mask (str): The user mask (nickname@host) of the command issuer.
|
||||||
target: The channel or user where the command was issued.
|
target (str): The channel or user where the command was issued.
|
||||||
args: Parsed command arguments.
|
args (dict): Parsed command arguments.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
%%upload [--mp3] <url>
|
%%upload [--mp3] <url>
|
||||||
@ -72,7 +83,7 @@ class UploadPlugin:
|
|||||||
if not url:
|
if not url:
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style("Usage: !upload [--mp3] <url>", fg="red", bold=True, reset=True)
|
ircstyle.style("Usage: !upload [--mp3] <url>", fg="red", bold=True, reset=True),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -83,20 +94,31 @@ class UploadPlugin:
|
|||||||
exc_msg = self._ensure_str(exc)
|
exc_msg = self._ensure_str(exc)
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Upload task error: {exc_msg}", fg="red", bold=True, reset=True)
|
ircstyle.style(f"Upload task error: {exc_msg}", fg="red", bold=True, reset=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def do_upload(self, url, target, mp3):
|
async def do_upload(self, url, target, mp3):
|
||||||
"""
|
"""
|
||||||
Download a file using yt-dlp and upload it to hardfiles.org.
|
Download a file using yt-dlp and upload it to hardfiles.org.
|
||||||
Handles binary data and non-UTF-8 strings to avoid decoding errors.
|
Handles binary data and non-UTF-8 strings to avoid decoding errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL of the file to download.
|
||||||
|
target (str): The channel or user to send messages to.
|
||||||
|
mp3 (bool): Whether to convert the downloaded file to MP3.
|
||||||
"""
|
"""
|
||||||
max_size = 100 * 1024 * 1024 # 100MB limit
|
max_size = 100 * 1024 * 1024 # 100MB limit
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
domain = parsed_url.netloc.lower()
|
domain = parsed_url.netloc.lower()
|
||||||
skip_check_domains = ("x.com", "instagram.com", "youtube.com", "youtu.be", "streamable.com")
|
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)
|
should_check_headers = not any(domain.endswith(d) for d in skip_check_domains)
|
||||||
|
|
||||||
if should_check_headers:
|
if should_check_headers:
|
||||||
@ -108,8 +130,10 @@ class UploadPlugin:
|
|||||||
target,
|
target,
|
||||||
ircstyle.style(
|
ircstyle.style(
|
||||||
f"Failed to fetch headers: HTTP {response.status}",
|
f"Failed to fetch headers: HTTP {response.status}",
|
||||||
fg="red", bold=True, reset=True
|
fg="red",
|
||||||
)
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
content_length = response.headers.get('Content-Length')
|
content_length = response.headers.get('Content-Length')
|
||||||
@ -118,15 +142,22 @@ class UploadPlugin:
|
|||||||
target,
|
target,
|
||||||
ircstyle.style(
|
ircstyle.style(
|
||||||
f"File size ({int(content_length) // (1024 * 1024)}MB) exceeds 100MB limit",
|
f"File size ({int(content_length) // (1024 * 1024)}MB) exceeds 100MB limit",
|
||||||
fg="red", bold=True, reset=True
|
fg="red",
|
||||||
)
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = self._ensure_str(e)
|
err_msg = self._ensure_str(e)
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Error during header check: {err_msg}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Error during header check: {err_msg}",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -137,11 +168,15 @@ class UploadPlugin:
|
|||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'concurrent_fragment_downloads': 5,
|
'concurrent_fragment_downloads': 5,
|
||||||
'postprocessors': [{
|
'postprocessors': [
|
||||||
'key': 'FFmpegExtractAudio',
|
{
|
||||||
'preferredcodec': 'mp3',
|
'key': 'FFmpegExtractAudio',
|
||||||
'preferredquality': '192',
|
'preferredcodec': 'mp3',
|
||||||
}] if mp3 else [],
|
'preferredquality': '192',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if mp3
|
||||||
|
else [],
|
||||||
}
|
}
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
@ -151,13 +186,20 @@ class UploadPlugin:
|
|||||||
err_msg = self._ensure_str(e)
|
err_msg = self._ensure_str(e)
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Info extraction failed: {err_msg}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Info extraction failed: {err_msg}", fg="red", bold=True, reset=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style("Error: Received non-UTF-8 output during info extraction", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
"Error: Received non-UTF-8 output during info extraction",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -167,8 +209,10 @@ class UploadPlugin:
|
|||||||
target,
|
target,
|
||||||
ircstyle.style(
|
ircstyle.style(
|
||||||
f"File size ({estimated_size // (1024 * 1024)}MB) exceeds 100MB limit",
|
f"File size ({estimated_size // (1024 * 1024)}MB) exceeds 100MB limit",
|
||||||
fg="red", bold=True, reset=True
|
fg="red",
|
||||||
)
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -178,13 +222,20 @@ class UploadPlugin:
|
|||||||
err_msg = self._ensure_str(e)
|
err_msg = self._ensure_str(e)
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Download failed: {err_msg}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Download failed: {err_msg}", fg="red", bold=True, reset=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style("Error: Received non-UTF-8 output during download", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
"Error: Received non-UTF-8 output during download",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -198,20 +249,43 @@ class UploadPlugin:
|
|||||||
description = self._ensure_str(info.get("description"))
|
description = self._ensure_str(info.get("description"))
|
||||||
|
|
||||||
if title:
|
if title:
|
||||||
metadata_parts.append(ircstyle.style(f"Title: {title}", fg="yellow", bold=True, reset=True))
|
metadata_parts.append(
|
||||||
|
ircstyle.style(f"Title: {title}", fg="yellow", bold=True, reset=True)
|
||||||
|
)
|
||||||
if uploader:
|
if uploader:
|
||||||
metadata_parts.append(ircstyle.style(f"Uploader: {uploader}", fg="purple", bold=True, reset=True))
|
metadata_parts.append(
|
||||||
|
ircstyle.style(f"Uploader: {uploader}", fg="purple", bold=True, reset=True)
|
||||||
|
)
|
||||||
if duration:
|
if duration:
|
||||||
metadata_parts.append(ircstyle.style(f"Duration: {self._format_duration(duration)}", fg="green", bold=True, reset=True))
|
metadata_parts.append(
|
||||||
|
ircstyle.style(
|
||||||
|
f"Duration: {self._format_duration(duration)}",
|
||||||
|
fg="green",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
if upload_date:
|
if upload_date:
|
||||||
formatted_date = f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:]}" if len(upload_date) == 8 else upload_date
|
formatted_date = (
|
||||||
metadata_parts.append(ircstyle.style(f"Upload Date: {formatted_date}", fg="aqua", bold=True, reset=True))
|
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:
|
if view_count is not None:
|
||||||
metadata_parts.append(ircstyle.style(f"Views: {view_count}", fg="royal", bold=True, reset=True))
|
metadata_parts.append(
|
||||||
|
ircstyle.style(f"Views: {view_count}", fg="royal", bold=True, reset=True)
|
||||||
|
)
|
||||||
if description:
|
if description:
|
||||||
if len(description) > 200:
|
if len(description) > 200:
|
||||||
description = description[:200] + "..."
|
description = description[:200] + "..."
|
||||||
metadata_parts.append(ircstyle.style(f"Description: {description}", fg="silver", reset=True))
|
metadata_parts.append(
|
||||||
|
ircstyle.style(f"Description: {description}", fg="silver", reset=True)
|
||||||
|
)
|
||||||
if metadata_parts:
|
if metadata_parts:
|
||||||
self.bot.privmsg(target, " | ".join(metadata_parts))
|
self.bot.privmsg(target, " | ".join(metadata_parts))
|
||||||
|
|
||||||
@ -219,7 +293,7 @@ class UploadPlugin:
|
|||||||
if not downloaded_files:
|
if not downloaded_files:
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style("No files downloaded", fg="red", bold=True, reset=True)
|
ircstyle.style("No files downloaded", fg="red", bold=True, reset=True),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -228,7 +302,12 @@ class UploadPlugin:
|
|||||||
if not downloaded_file or not os.path.exists(downloaded_file):
|
if not downloaded_file or not os.path.exists(downloaded_file):
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Downloaded file not found: {downloaded_file}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Downloaded file not found: {downloaded_file}",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -238,8 +317,10 @@ class UploadPlugin:
|
|||||||
target,
|
target,
|
||||||
ircstyle.style(
|
ircstyle.style(
|
||||||
f"File size ({file_size // (1024 * 1024)}MB) exceeds 100MB limit",
|
f"File size ({file_size // (1024 * 1024)}MB) exceeds 100MB limit",
|
||||||
fg="red", bold=True, reset=True
|
fg="red",
|
||||||
)
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -252,13 +333,20 @@ class UploadPlugin:
|
|||||||
'file',
|
'file',
|
||||||
file_content,
|
file_content,
|
||||||
filename=os.path.basename(downloaded_file),
|
filename=os.path.basename(downloaded_file),
|
||||||
content_type='application/octet-stream'
|
content_type='application/octet-stream',
|
||||||
)
|
)
|
||||||
async with session.post('https://hardfiles.org/', data=form, allow_redirects=False) as resp:
|
async with session.post(
|
||||||
|
'https://hardfiles.org/', data=form, allow_redirects=False
|
||||||
|
) as resp:
|
||||||
if resp.status not in [200, 201, 302, 303]:
|
if resp.status not in [200, 201, 302, 303]:
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Upload failed: HTTP {resp.status}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Upload failed: HTTP {resp.status}",
|
||||||
|
fg="red",
|
||||||
|
bold=True,
|
||||||
|
reset=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
raw_response = await resp.read()
|
raw_response = await resp.read()
|
||||||
@ -267,21 +355,29 @@ class UploadPlugin:
|
|||||||
upload_url = self.extract_url_from_response(response_text) or "Unknown URL"
|
upload_url = self.extract_url_from_response(response_text) or "Unknown URL"
|
||||||
upload_url = self._ensure_str(upload_url)
|
upload_url = self._ensure_str(upload_url)
|
||||||
response_msg = (
|
response_msg = (
|
||||||
ircstyle.style("Upload successful: ", fg="green", bold=True, reset=True) +
|
ircstyle.style("Upload successful: ", fg="green", bold=True, reset=True)
|
||||||
ircstyle.style(upload_url, fg="blue", underline=True, reset=True)
|
+ ircstyle.style(upload_url, fg="blue", underline=True, reset=True)
|
||||||
)
|
)
|
||||||
self.bot.privmsg(target, response_msg)
|
self.bot.privmsg(target, response_msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = self._ensure_str(e)
|
err_msg = self._ensure_str(e)
|
||||||
self.bot.privmsg(
|
self.bot.privmsg(
|
||||||
target,
|
target,
|
||||||
ircstyle.style(f"Error during file upload: {err_msg}", fg="red", bold=True, reset=True)
|
ircstyle.style(
|
||||||
|
f"Error during file upload: {err_msg}", fg="red", bold=True, reset=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def extract_url_from_response(self, response_text):
|
def extract_url_from_response(self, response_text):
|
||||||
"""
|
"""
|
||||||
Extract the first URL found in the response text.
|
Extract the first URL found in the response text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_text (str): The response text to search for URLs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The first URL found in the response text, or None if no URL is found.
|
||||||
"""
|
"""
|
||||||
match = re.search(r'https?://\S+', response_text)
|
match = re.search(r'https?://\S+', response_text)
|
||||||
return match.group(0) if match else None
|
return match.group(0) if match else None
|
||||||
@ -289,8 +385,14 @@ class UploadPlugin:
|
|||||||
def _format_duration(self, seconds):
|
def _format_duration(self, seconds):
|
||||||
"""
|
"""
|
||||||
Convert seconds into a human-readable duration string.
|
Convert seconds into a human-readable duration string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (int): The duration in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The formatted duration string.
|
||||||
"""
|
"""
|
||||||
seconds = int(seconds)
|
seconds = int(seconds)
|
||||||
m, s = divmod(seconds, 60)
|
m, s = divmod(seconds, 60)
|
||||||
h, m = divmod(m, 60)
|
h, m = divmod(m, 60)
|
||||||
return f"{h}h {m}m {s}s" if h else f"{m}m {s}s"
|
return f"{h}h {m}m {s}s" if h else f"{m}m {s}s"
|
@ -1,8 +1,33 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from irc3.plugins.command import command
|
"""
|
||||||
from irc3.compat import Queue
|
IRC3 Bot Plugin: Urban Dictionary Search
|
||||||
|
|
||||||
|
This plugin for an IRC bot allows users to search for terms on Urban Dictionary and post the results to an IRC channel.
|
||||||
|
It uses aiohttp for asynchronous HTTP requests and a queue to manage search requests.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Asynchronously fetches definitions from Urban Dictionary.
|
||||||
|
- Enqueues search requests and processes them one at a time.
|
||||||
|
- Formats and posts the definition, example, and permalink to the IRC channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@command
|
||||||
|
def urban(self, mask, target, args):
|
||||||
|
%%urban <term>...
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-12
|
||||||
|
"""
|
||||||
|
|
||||||
import irc3
|
import irc3
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from irc3.plugins.command import command
|
||||||
|
from irc3.compat import Queue
|
||||||
|
|
||||||
|
|
||||||
@irc3.plugin
|
@irc3.plugin
|
||||||
class UrbanDictionaryPlugin:
|
class UrbanDictionaryPlugin:
|
||||||
@ -13,6 +38,12 @@ class UrbanDictionaryPlugin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
|
"""
|
||||||
|
Initialize the plugin with bot reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.queue = Queue() # Queue for managing search requests
|
self.queue = Queue() # Queue for managing search requests
|
||||||
self.session = None # aiohttp session initialized lazily
|
self.session = None # aiohttp session initialized lazily
|
||||||
@ -22,6 +53,13 @@ class UrbanDictionaryPlugin:
|
|||||||
def urban(self, mask, target, args):
|
def urban(self, mask, target, args):
|
||||||
"""
|
"""
|
||||||
Search Urban Dictionary for a term.
|
Search Urban Dictionary for a term.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (str): The user mask (nickname@host) of the command issuer.
|
||||||
|
target (str): The channel or user where the command was issued.
|
||||||
|
args (dict): Command arguments.
|
||||||
|
|
||||||
|
Usage:
|
||||||
%%urban <term>...
|
%%urban <term>...
|
||||||
"""
|
"""
|
||||||
term = ' '.join(args['<term>'])
|
term = ' '.join(args['<term>'])
|
||||||
|
@ -1,5 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
A plugin for fetching and displaying titles of URLs shared in IRC messages.
|
IRC3 Bot Plugin: URL Title Fetcher
|
||||||
|
|
||||||
|
This plugin for an IRC bot fetches and displays the titles of URLs shared in IRC messages.
|
||||||
|
It uses aiohttp for asynchronous HTTP requests and lxml for HTML parsing.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Listens for PRIVMSG events in the IRC channel.
|
||||||
|
- Extracts URLs from messages and fetches their titles.
|
||||||
|
- Posts the title and URL back to the IRC channel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
======
|
||||||
|
To use this module, load it as a plugin in your IRC bot configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@event
|
||||||
|
def on_privmsg(self, mask, event, target, data):
|
||||||
|
# Extract URLs from messages and fetch their titles.
|
||||||
|
|
||||||
|
Author: Zodiac
|
||||||
|
Date: 2025-02-13
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@ -11,6 +32,7 @@ from irc3 import event
|
|||||||
from irc3.compat import Queue
|
from irc3.compat import Queue
|
||||||
|
|
||||||
|
|
||||||
|
@irc3.plugin
|
||||||
class URLTitlePlugin:
|
class URLTitlePlugin:
|
||||||
"""
|
"""
|
||||||
A plugin to fetch and display the titles of URLs shared in IRC messages.
|
A plugin to fetch and display the titles of URLs shared in IRC messages.
|
||||||
@ -29,9 +51,9 @@ class URLTitlePlugin:
|
|||||||
bot (irc3.IrcBot): The IRC bot instance.
|
bot (irc3.IrcBot): The IRC bot instance.
|
||||||
"""
|
"""
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.url_queue = Queue()
|
self.url_queue = Queue() # Queue for managing URL processing
|
||||||
self.session = aiohttp.ClientSession(loop=self.bot.loop)
|
self.session = aiohttp.ClientSession(loop=self.bot.loop)
|
||||||
self.bot.create_task(self.process_urls())
|
self.bot.create_task(self.process_urls()) # Start URL processor
|
||||||
|
|
||||||
@event(irc3.rfc.PRIVMSG)
|
@event(irc3.rfc.PRIVMSG)
|
||||||
async def on_privmsg(self, mask, event, target, data):
|
async def on_privmsg(self, mask, event, target, data):
|
||||||
@ -72,12 +94,7 @@ class URLTitlePlugin:
|
|||||||
)
|
)
|
||||||
await self.bot.privmsg(target, formatted_message)
|
await self.bot.privmsg(target, formatted_message)
|
||||||
else:
|
else:
|
||||||
# Format the error message with colors and styles
|
# Handle cases where no title is found
|
||||||
# formatted_message = (
|
|
||||||
# f"\x02\x034Error:\x03 Could not find a title for: "
|
|
||||||
# f"\x0311{url}\x03"
|
|
||||||
# )
|
|
||||||
# await self.bot.privmsg(target, formatted_message)
|
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bot.log.error(f"Error processing URL {url}: {e}")
|
self.bot.log.error(f"Error processing URL {url}: {e}")
|
||||||
|
Loading…
Reference in New Issue
Block a user