Compare commits

...

22 Commits

Author SHA1 Message Date
44b3fda6de youtube: add support for /shorts/ URLs 2025-04-14 19:27:15 +00:00
f80c89ba09 youtube: add support for /live/ URLs 2025-04-08 09:02:17 +00:00
132d6a19ca Revert "useless: add vampir trigger"
This reverts commit ade97db922.

Link is down, so remove the trigger.
2025-02-27 13:55:14 +00:00
27c537b15a coins: fix 24h change 2025-02-25 21:08:16 +00:00
6a9bf7456b requirements: update lxml to 5.3 2025-02-04 12:46:12 +00:00
0edbc7a0f1 mcmaniac: fix a typo 2025-01-23 23:14:34 +00:00
360fd4d164 coins: display currency name in upper case 2024-12-17 00:17:38 +00:00
91abf2bf93 coins: switch to coingecko api
fix #19
2024-12-11 00:08:54 +00:00
3f7d1ae13c youtube: limit search to 1 video result 2024-06-02 15:31:24 +00:00
586a109a16 drugs, urlinfo, youtube: fix 'invalid escape sequence' SyntaxWarnings 2024-06-02 15:30:20 +00:00
6986798245 ascii: remove buddha command 2024-04-17 10:14:39 +00:00
b1100db649 urlinfo: make .* in front of the URL non-greedy
This is done because URLs may contain one or more other URLs that match
the url pattern (e.g. https://web.archive.org links). Because .* is
greedy by default, this caused only the last matching URL to be
captured, instead of the full URL.
2023-08-05 13:42:46 +00:00
1149417b56 quotes: replace non-functional print() with self.log.error() 2023-07-25 22:28:08 +00:00
f50fe88a14 linux: handle JSONDecodeError 2023-07-21 11:47:54 +00:00
fa51899a24 urlinfo: catch superclass RequestException instead of each exception individually 2023-07-21 11:44:20 +00:00
d59e170e8b linux: use json kernel.org release feeed
Reading the releases from the json feed is much simpler and it allows us
to drop the dependency on feedparser.
2022-11-08 00:25:05 +00:00
cb1cc4439a chore: update feedparser
The old feedparser 5.x versions are incompatible with recent setuptools
versions and can't be installed anymore
2022-11-07 20:24:26 +00:00
ffa64f509c linux: fix two SyntaxWarnings 2022-11-07 20:20:11 +00:00
9ba11a50cd youtube: fix a SyntaxWarning 2022-09-17 10:44:00 +00:00
9c8203d433 youtube: use "Return YouTube Dislike" api for the dislike count 2021-12-14 20:45:08 +00:00
9ba23e9a29 linux: remove double spaces in stallman quote 2021-12-06 20:07:29 +00:00
5fe240db8b mcmaniac: print confirmation when new entry is added 2021-12-06 02:51:00 +00:00
10 changed files with 103 additions and 105 deletions

View File

@ -9,33 +9,6 @@ from . import Plugin
class Ascii(Plugin): class Ascii(Plugin):
@command
def buddha(self, mask: IrcString, target: IrcString, args: Dict):
"""prints a giant swastika
%%buddha
"""
for line in (
'░░░░░░░░░░░░░░░▄▀▄░░░░░░░░░░░░░░░',
'░░░░░░░░░░░░░▄▀░░░▀▄░░░░░░░░░░░░░',
'░░░░░░░░░░░▄▀░░░░▄▀█░░░░░░░░░░░░░',
'░░░░░░░░░▄▀░░░░▄▀░▄▀░▄▀▄░░░░░░░░░',
'░░░░░░░▄▀░░░░▄▀░▄▀░▄▀░░░▀▄░░░░░░░',
'░░░░░░░█▀▄░░░░▀█░▄▀░░░░░░░▀▄░░░░░',
'░░░▄▀▄░▀▄░▀▄░░░░▀░░░░▄█▄░░░░▀▄░░░',
'░▄▀░░░▀▄░▀▄░▀▄░░░░░▄▀░█░▀▄░░░░▀▄░',
'░█▀▄░░░░▀▄░█▀░░░░░░░▀█░▀▄░▀▄░▄▀█░',
'░▀▄░▀▄░░░░▀░░░░▄█▄░░░░▀▄░▀▄░█░▄▀░',
'░░░▀▄░▀▄░░░░░▄▀░█░▀▄░░░░▀▄░▀█▀░░░',
'░░░░░▀▄░▀▄░▄▀░▄▀░█▀░░░░▄▀█░░░░░░░',
'░░░░░░░▀▄░█░▄▀░▄▀░░░░▄▀░▄▀░░░░░░░',
'░░░░░░░░░▀█▀░▄▀░░░░▄▀░▄▀░░░░░░░░░',
'░░░░░░░░░░░░░█▀▄░▄▀░▄▀░░░░░░░░░░░',
'░░░░░░░░░░░░░▀▄░█░▄▀░░░░░░░░░░░░░',
'░░░░░░░░░░░░░░░▀█▀░░░░░░░░░░░░░░░'
):
self.bot.privmsg(target, line)
@command @command
def rotschwuchtel(self, mask: IrcString, target: IrcString, args: Dict): def rotschwuchtel(self, mask: IrcString, target: IrcString, args: Dict):
"""prints a giant hammer and sickle """prints a giant hammer and sickle

View File

@ -10,12 +10,12 @@ def _currency(name, prefix=None, suffix=None):
def tostr(value): def tostr(value):
return '{prefix}{value:,.2f}{suffix}'.format( return '{prefix}{value:,.2f}{suffix}'.format(
prefix=prefix or '', prefix=prefix or '',
suffix=suffix or ('' if (prefix or suffix) else (' ' + name)), suffix=suffix or ('' if (prefix or suffix) else (' ' + name.upper())),
value=value) value=value)
return tostr return tostr
class Coins(Plugin): class Coins(Plugin):
API_URL = 'https://api.cryptowat.ch/markets/{market}/{crypto}{currency}/summary' API_URL = 'https://api.coingecko.com/api/v3/simple/price'
CURRENCIES = { CURRENCIES = {
'usd': _currency('usd', prefix='$'), 'usd': _currency('usd', prefix='$'),
@ -23,60 +23,89 @@ class Coins(Plugin):
'eth': _currency('eth', suffix='Ξ'), 'eth': _currency('eth', suffix='Ξ'),
'btc': _currency('btc', suffix='฿'), 'btc': _currency('btc', suffix='฿'),
'xmr': _currency('xmr'), 'xmr': _currency('xmr'),
'xrp': _currency('xrp')
}
CRYPTO_IDS = {
'btc': 'bitcoin',
'eth': 'ethereum',
'xmr': 'monero',
'xrp': 'ripple'
} }
@command @command
def btc(self, mask: IrcString, target: IrcString, args: Dict): def btc(self, mask: IrcString, target: IrcString, args: Dict):
"""Gets the Bitcoin values from cryptowatch. """Gets the Bitcoin values from coingecko.
%%btc [<currency>] %%btc [<currency>]
""" """
return self.cryptowat_summary('btc', 'coinbase', args) return self.coingecko_summary('btc', args)
@command @command
def eth(self, mask: IrcString, target: IrcString, args: Dict): def eth(self, mask: IrcString, target: IrcString, args: Dict):
"""Gets the Ethereum values from cryptowatch. """Gets the Ethereum values from coingecko.
%%eth [<currency>] %%eth [<currency>]
""" """
return self.cryptowat_summary('eth', 'coinbase', args) return self.coingecko_summary('eth', args)
@command @command
def xmr(self, mask: IrcString, target: IrcString, args: Dict): def xmr(self, mask: IrcString, target: IrcString, args: Dict):
"""Gets the Monero values from cryptowatch. """Gets the Monero values from coingecko.
%%xmr [<currency>] %%xmr [<currency>]
""" """
return self.cryptowat_summary('xmr', 'bitfinex', args) return self.coingecko_summary('xmr', args)
@command
def xrp(self, mask: IrcString, target: IrcString, args: Dict):
"""Gets the Ripple values from coingecko.
%%xrp [<currency>]
"""
return self.coingecko_summary('xrp', args)
def coingecko_summary(self, crypto: str, args: Dict):
crypto_id = self.CRYPTO_IDS.get(crypto)
if crypto_id is None:
raise ValueError
def cryptowat_summary(self, crypto: str, market: str, args: Dict):
currency = args.get('<currency>', 'eur').lower() currency = args.get('<currency>', 'eur').lower()
def format_response(response: str) -> str:
return f'\x02[{crypto.upper()}]\x02 {response}'
if currency not in self.CURRENCIES or crypto == currency: if currency not in self.CURRENCIES or crypto == currency:
return '\x02[{}]\x02 Can\'t convert or invalid currency: {}'.format(crypto.upper(), currency) return format_response(f'Can\'t convert or invalid currency: {currency}')
def get_data(cur): res = requests.get(self.API_URL, params={
return requests.get(self.API_URL.format(crypto=crypto, market=market, currency=cur)) 'ids': crypto_id,
'vs_currencies': currency,
'include_market_cap': 'true',
'include_24hr_vol': 'true',
'include_24hr_change': 'true'
})
data = get_data(currency) if res.status_code == 429:
if not data and currency != 'usd': return format_response('Rate Limit exceeded')
data = get_data('usd')
currency = 'usd' if res.status_code != 200:
if not data: return format_response('API Error')
return '\x02[{}]\x02 No data received'.format(crypto)
data = res.json()[crypto_id]
if currency not in data:
return format_response('No data received')
to_currency = self.CURRENCIES[currency] to_currency = self.CURRENCIES[currency]
result = data.json()['result'] return format_response(
return '\x02[{crypto}]\x02 ' \ 'Price: \x02\x0307{price}\x0F - ' \
'Current: \x02\x0307{last}\x0F - ' \
'High: \x02\x0303{high}\x0F - ' \
'Low: \x02\x0304{low}\x0F - ' \
'Change: \x02\x0307{change:,.2f}%\x0F - ' \ 'Change: \x02\x0307{change:,.2f}%\x0F - ' \
'Volume: \x02\x0307{volume}\x0F' \ 'Volume: \x02\x0307{volume}\x0F - ' \
'Market Cap: \x02\x0307{market_cap}\x0F' \
.format(crypto=crypto.upper(), .format(crypto=crypto.upper(),
last=to_currency(result['price']['last']), price=to_currency(data[currency]),
high=to_currency(result['price']['high']), change=data[f'{currency}_24h_change'],
low=to_currency(result['price']['low']), volume=data[f'{currency}_24h_vol'],
change=result['price']['change']['percentage'] * 100, market_cap=to_currency(data[f'{currency}_market_cap'])))
volume=result['volume'])

View File

@ -113,4 +113,4 @@ class Drugs(Plugin):
%%meth [<nick>] %%meth [<nick>]
""" """
self.bot.action(target, 'legt {} eine dicke Line Meth \________'.format(args.get('<nick>', mask.nick))) self.bot.action(target, r'legt {} eine dicke Line Meth \________'.format(args.get('<nick>', mask.nick)))

View File

@ -2,7 +2,7 @@
import random import random
import irc3 import irc3
import feedparser import requests
from docopt import Dict from docopt import Dict
from irc3.plugins.command import command from irc3.plugins.command import command
from irc3.utils import IrcString from irc3.utils import IrcString
@ -19,18 +19,18 @@ GNU_LINUX = """I'd Just Like To Interject For A Moment. What you're referring
class Linux(Plugin): class Linux(Plugin):
KERNEL_FEED = 'https://www.kernel.org/feeds/kdist.xml' KERNEL_FEED = 'https://www.kernel.org/releases.json'
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :(?:.* )?(debian|ubuntu|apt|dpkg|flash)(?: .*|$)') @irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :(?:.* )?(debian|ubuntu|apt|dpkg|flash)(?: .*|$)')
def debillian(self, target: str): def debillian(self, target: str):
"""Annoying RE trigger for debian with variable count of E.""" """Annoying RE trigger for debian with variable count of E."""
if random.randint(0, 3) is 0: if random.randint(0, 3) == 0:
self.bot.privmsg(target, re_generator()) self.bot.privmsg(target, re_generator())
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?<!gnu[/+])linux(?! kernel).*') @irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?<!gnu[/+])linux(?! kernel).*')
def linux(self, target: str): def linux(self, target: str):
"""Super annoying, useless 'Stallman is mad' trigger.""" """Super annoying, useless 'Stallman is mad' trigger."""
if random.randint(0, 12) is 0: if random.randint(0, 12) == 0:
self.bot.privmsg(target, GNU_LINUX) self.bot.privmsg(target, GNU_LINUX)
@command @command
@ -39,21 +39,17 @@ class Linux(Plugin):
%%kernel %%kernel
""" """
feed = feedparser.parse(self.KERNEL_FEED) try:
res = requests.get(self.KERNEL_FEED, timeout=10).json()
# Cancel if no feed or entries except requests.exceptions.RequestException:
if not feed or not feed.get('entries'): return '\x02[Kernel]\x02 Error fetching kernel.org releases'
self.log.error('Error fetching kernel.org releases feed') except requests.exceptions.JSONDecodeError:
return return '\x02[Kernel]\x02 Error decoding kernel.org releases'
# Make list of releases # Make list of releases
releases = [] releases = []
for e in feed['entries']: for release in res['releases']:
version, branch = e['title'].split(': ') releases.append('\x02{}\x02 ({}{})'.format(release['version'], release['moniker'],
', \x1DEOL\x1D' if release['iseol'] else ''))
if '(EOL)' in e['description']:
branch = '{}, \x1DEOL\x1D'.format(branch)
releases.append('\x02{}\x02 ({})'.format(version, branch))
return '\x02[Kernel]\x02 {}'.format(', '.join(releases)) return '\x02[Kernel]\x02 {}'.format(', '.join(releases))

View File

@ -49,8 +49,8 @@ class McManiac(DatabasePlugin):
if result: if result:
return '[{rank}/{total}] {item}'.format(**result) return '[{rank}/{total}] {item}'.format(**result)
@irc3.event(r'^:(?P<mask>\S+) PRIVMSG \S+ :.*?\b(?P<item>Mc\S+iaC)\b.*') @irc3.event(r'^:(?P<mask>\S+) PRIVMSG (?P<channel>#\S+) :.*?\b(?P<item>Mc\S+iaC)\b.*')
def save(self, mask: str, item: str): def save(self, mask: str, channel: str, item: str):
if IrcString(mask).nick != self.bot.nick: if IrcString(mask).nick != self.bot.nick:
with self.con.cursor() as cur: with self.con.cursor() as cur:
cur.execute(''' cur.execute('''
@ -59,5 +59,11 @@ class McManiac(DatabasePlugin):
VALUES VALUES
(%s) (%s)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
RETURNING
(SELECT (count(*) + 1) AS total FROM mcmaniacs)
''', [item]) ''', [item])
self.con.commit() self.con.commit()
result = cur.fetchone()
if result is None:
return
self.bot.privmsg(channel, f"{result['total']}. mcmaniac added: {item}")

View File

@ -87,7 +87,7 @@ class Quotes(DatabasePlugin):
self.con.commit() self.con.commit()
except Error as ex: except Error as ex:
# Rollback transaction on error # Rollback transaction on error
print(ex) self.log.error(ex)
self.con.rollback() self.con.rollback()
else: else:
query = None query = None

View File

@ -9,17 +9,17 @@ from . import Plugin
class URLInfo(Plugin): class URLInfo(Plugin):
BLACKLIST = [ BLACKLIST = [
"^https?:\/\/(?:www\.)?youtube\.com", r"^https?:\/\/(?:www\.)?youtube\.com",
"^https?:\/\/youtu\.be", r"^https?:\/\/youtu\.be",
"^https?:\/\/w0bm\.com", r"^https?:\/\/w0bm\.com",
"^https?:\/\/f0ck\.me", r"^https?:\/\/f0ck\.me",
"^https?:\/\/(?:(?:vid|img|thumb)\.)?pr0gramm\.com" r"^https?:\/\/(?:(?:vid|img|thumb)\.)?pr0gramm\.com"
] ]
# set the size limit to 2 MB so we don't fully download too large resources # set the size limit to 2 MB so we don't fully download too large resources
SIZE_LIMIT = 2 * 1024 ** 2 SIZE_LIMIT = 2 * 1024 ** 2
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?P<url>https?:\/\/\S+\.\S+).*') @irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*?(?P<url>https?:\/\/\S+\.\S+).*')
def url_parser(self, target: str, url: str): def url_parser(self, target: str, url: str):
for regex in self.BLACKLIST: for regex in self.BLACKLIST:
if re.match(regex, url): if re.match(regex, url):
@ -39,7 +39,7 @@ class URLInfo(Plugin):
if size >= self.SIZE_LIMIT: if size >= self.SIZE_LIMIT:
return return
bytes_io.write(chunk) bytes_io.write(chunk)
except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ReadTimeout, requests.exceptions.TooManyRedirects): except requests.exceptions.RequestException:
return return
bytes_io.seek(0) bytes_io.seek(0)

View File

@ -471,14 +471,6 @@ class Useless(DatabasePlugin):
nick = args.get('<nick>', mask.nick) nick = args.get('<nick>', mask.nick)
self.bot.action(target, 'hustet {0} in\'s Gesicht'.format(nick, nick)) self.bot.action(target, 'hustet {0} in\'s Gesicht'.format(nick, nick))
@command
def vampir(self, mask: IrcString, target: IrcString, args: Dict):
"""Sends a url with the cute vampire cat :3
%%vampir
"""
return 'https://files.catbox.moe/ma2dfs.png'
@command @command
def hebamme(self, mask: IrcString, target: IrcString, args: Dict): def hebamme(self, mask: IrcString, target: IrcString, args: Dict):
"""Tells you to get the fuck outta this group """Tells you to get the fuck outta this group

View File

@ -15,7 +15,8 @@ from .utils import date_from_iso
class YouTube(Plugin): class YouTube(Plugin):
URL = 'https://www.googleapis.com/youtube/v3' URL = 'https://www.googleapis.com/youtube/v3'
API = '{}/videos?part=snippet,statistics,contentDetails'.format(URL) API = '{}/videos?part=snippet,statistics,contentDetails'.format(URL)
SEARCH = '{}/search?part=id'.format(URL) SEARCH = '{}/search?type=video&maxResults=1&part=id'.format(URL)
RETURN_YOUTUBE_DISLIKE_API = 'https://returnyoutubedislikeapi.com/votes'
def get_video_data(self, video_id: str): def get_video_data(self, video_id: str):
"""Requests the infos for a video id and formats them.""" """Requests the infos for a video id and formats them."""
@ -24,13 +25,15 @@ class YouTube(Plugin):
if not data.get('items'): if not data.get('items'):
return return
return_youtube_dislike_response = requests.get(self.RETURN_YOUTUBE_DISLIKE_API, params={'videoId': video_id}).json()
item = data['items'][0] item = data['items'][0]
date = date_from_iso(item['snippet']['publishedAt']) date = date_from_iso(item['snippet']['publishedAt'])
length = re.findall('(\d+[DHMS])', item['contentDetails']['duration']) length = re.findall(r'(\d+[DHMS])', item['contentDetails']['duration'])
views = int(item['statistics'].get('viewCount', 0)) views = int(item['statistics'].get('viewCount', 0))
likes = int(item['statistics'].get('likeCount', 0)) likes = int(item['statistics'].get('likeCount', 0))
dislikes = int(item['statistics'].get('dislikeCount', 0)) dislikes = int(return_youtube_dislike_response.get('dislikes', 0))
try: try:
score = 100 * float(likes) / (likes + dislikes) score = 100 * float(likes) / (likes + dislikes)
@ -50,7 +53,7 @@ class YouTube(Plugin):
views=views, views=views,
date=date.strftime('%d.%m.%Y %H:%M:%S UTC')) date=date.strftime('%d.%m.%Y %H:%M:%S UTC'))
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?:youtube.*?(?:v=|/v/)' @irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?:youtube.*?(?:v=|/v/|/live/|/shorts/)'
r'|youtu\.be/)(?P<video_id>[-_a-zA-Z0-9]+).*') r'|youtu\.be/)(?P<video_id>[-_a-zA-Z0-9]+).*')
def youtube_parser(self, target: str, video_id: str): def youtube_parser(self, target: str, video_id: str):
data = self.get_video_data(video_id) data = self.get_video_data(video_id)
@ -67,7 +70,7 @@ class YouTube(Plugin):
if 'error' in data: if 'error' in data:
return '\x02[YouTube]\x02 Error performing search' return '\x02[YouTube]\x02 Error performing search'
elif data['pageInfo']['totalResults'] is 0: elif data['pageInfo']['totalResults'] == 0:
return '\x02[YouTube]\x02 No results found' return '\x02[YouTube]\x02 No results found'
else: else:
video_id = data['items'][0]['id']['videoId'] video_id = data['items'][0]['id']['videoId']

View File

@ -6,6 +6,5 @@ aiocron~=1.3
psycopg2~=2.8 psycopg2~=2.8
requests~=2.22 requests~=2.22
GitPython~=3.0 GitPython~=3.0
feedparser~=5.2
python-dotenv~=0.17.1 python-dotenv~=0.17.1
lxml~=4.6 lxml~=5.3