diff options
-rw-r--r-- | misc.py | 20 | ||||
-rw-r--r-- | piston.py | 117 | ||||
-rw-r--r-- | spellcheck.py.old (renamed from spellcheck.py) | 0 | ||||
-rw-r--r-- | wild.py | 9 | ||||
-rw-r--r-- | yt.py | 146 |
5 files changed, 282 insertions, 10 deletions
@@ -192,17 +192,25 @@ def coinflip_cmd(irc, hostmask, is_admin, args): reply(irc, hostmask, args, msg) +def get_char_name(char): + # Hack to get the name of control characters. U+2400 is "SYMBOL FOR NULL" + # and this function will remove the leading "SYMBOL FOR " prefix when a + # null byte is passed to it. + if char <= '\x1f': + char = chr(ord(char) + 0x2400) + return unicodedata.name(char)[11:] + return unicodedata.name(char, 'Unknown') + + @register_command('chr', 'unicode', 'u') def unicode_cmd(irc, hostmask, is_admin, args): chars = args[1] - if len(chars) > 5: - msg = f'You must provide 1-5 characters, not {len(chars)}.' + if len(chars) > 10: + msg = f'You must provide 1-10 characters, not {len(chars)}.' elif not chars: msg = 'Usage: .unicode <character>' else: - msg = ', '.join( - f'U+{ord(char):04X} - {unicodedata.name(char, "Unknown")}' - for char in chars - ) + msg = ', '.join(f'U+{ord(char):04X} - {get_char_name(char)}' + for char in chars) reply(irc, hostmask, args, msg) diff --git a/piston.py b/piston.py new file mode 100644 index 0000000..3b8504d --- /dev/null +++ b/piston.py @@ -0,0 +1,117 @@ +# +# Piston commands +# +# Copyright © 2023 by luk3yx +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +import requests + +PISTON_URL = 'https://emkc.org/api/v2/piston/execute' + + +@register_command('sh', 'bash') +def sh(irc, hostmask, is_admin, args): + res = requests.post(PISTON_URL, json={ + 'language': 'bash', + 'version': '*', + 'files': [{'name': 'main.sh', 'content': args[1]}] + }) + output = res.json()['run']['output'].rstrip('\n') + irc.msg(args[0], f'{hostmask[0]}: {output or "(no output)"}') + + +LUA_CODE = r""" +local f = assert(io.open("code.lua", "rb")) +local code = f:read("*a") +f:close() +local f = load("return " .. code) +if f then + print(f()) +else + dofile("code.lua") +end +""" + + +@register_command('lua') +def lua(irc, hostmask, is_admin, args): + res = requests.post(PISTON_URL, json={ + 'language': 'lua', + 'version': '*', + 'files': [ + {'name': 'init.lua', 'content': LUA_CODE}, + {'name': 'code.lua', 'content': args[1]} + ] + }) + output = res.json()['run']['output'].rstrip('\n') + irc.msg(args[0], f'{hostmask[0]}: {output or "(no output)"}') + + +PYTHON_CODE = r""" +import ast, builtins + +with open('code.py', 'r') as f: + code = f.read() + + +# Automatically import libraries where possible +class _BuiltinsWrapper(dict): + __slots__ = () + def __missing__(self, key): + try: + return __import__(key) + except ImportError: + raise NameError(f'name {key!r} is not defined') + + +env = {'__builtins__': _BuiltinsWrapper(builtins.__dict__), 'code': code} + + +try: + ast.parse(code, mode='eval') +except SyntaxError: + exec(code, env) +else: + print(eval(code, env)) +""" + + +@register_command('py', 'py3', 'python', 'python3') +def py3(irc, hostmask, is_admin, args): + res = requests.post(PISTON_URL, json={ + 'language': 'python', + 'version': '3', + 'files': [ + {'name': 'main.py', 'content': PYTHON_CODE}, + {'name': 'code.py', 'content': args[1]} + ] + }) + run = res.json()['run'] + output = run['output'].rstrip('\n') + if run['code'] == 1: + output = output.rsplit('\n', 1)[-1] + irc.msg(args[0], f'{hostmask[0]}: {output or "(no output)"}') + + +@register_command('hs', 'haskell') +def hs(irc, hostmask, is_admin, args): + res = requests.post(PISTON_URL, json={ + 'language': 'haskell', + 'version': '*', + 'files': [{'name': 'main.hs', 'content': args[1]}] + }) + output = res.json()['run']['output'].rstrip('\n') + irc.msg(args[0], f'{hostmask[0]}: {output or "(no output)"}') diff --git a/spellcheck.py b/spellcheck.py.old index b3a2b04..b3a2b04 100644 --- a/spellcheck.py +++ b/spellcheck.py.old @@ -147,9 +147,9 @@ class WildComma(WildThing): return False, 'After a long day of consideration, you give up and' \ ' allow the wild {} to win!' -@CustomWildThing('Andrew') -class WildAndrew(WildThing): - options2 = ("hijack Andrew's WeeChat session",) +@CustomWildThing('Adeline') +class WildAdeline(WildThing): + options2 = ("hijack Adeline's WeeChat session",) options3 = ('rickroll',) def choice2(self, i): if i > 3: @@ -160,7 +160,8 @@ class WildAndrew(WildThing): def choice3(self, i): if i > 3: return 'You catch the wild {} off-guard and rickroll them!' - return 'The wild {} decides that they are \2never gonna give you up\2!' + return False, ('The wild {} decides that they are \2never gonna give ' + 'you up\2!') data = {} @@ -0,0 +1,146 @@ +# +# YouTube video info command +# +# Copyright © 2023 by luk3yx +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +import math, requests, random, time, traceback + + +# Default values so that the command still works if api.invidious.io is down +cache_expiry = -math.inf +instances = ['yewtu.be'] +api_instances = ['i.psf.lt'] + + +def fetch_instances(irc, args): + global cache_expiry, instances, api_instances + + # Refresh the cache at most once per day + if time.monotonic() >= cache_expiry or not instances or not api_instances: + try: + resp = requests.get('https://api.invidious.io/instances.json') + resp.raise_for_status() + + new_instances = [] + new_api_instances = [] + for _, info in resp.json(): + if (info['type'] != 'https' or not info.get('stats') or + not info['stats'].get('openRegistrations')): + continue + + new_instances.append(info['uri']) + if info['api']: + new_api_instances.append(info['uri']) + + if new_instances and new_api_instances: + cache_expiry = time.monotonic() + 86400 + instances = new_instances + api_instances = new_api_instances + except (requests.RequestException, IndexError, ValueError) as exc: + # This is okay, just use the cache for now + traceback.print_exc() + irc.notice(args[0], f'{exc.__class__.__name__} when fetching ' + f'Invidious instance list, using cache.') + + return instances, api_instances + + +RED = '\x0304' +GREEN = '\x0303' + + +def get_url_and_metadata(irc, args, video_id): + instances, api_instances = fetch_instances(irc, args) + api_url = f'{random.choice(api_instances)}' + res = requests.get(f'{api_url}/api/v1/videos/{video_id}', params={ + 'fields': 'title,liveNow,lengthSeconds,author,publishedText,error' + }) + return f'{random.choice(instances)}/watch?v={video_id}', res.json() + + +def format_length(seconds): + minutes = seconds // 60 + seconds %= 60 + if minutes < 60: + return f'{minutes}:{seconds:02}' + return f'{minutes // 60}:{minutes % 60:02}:{seconds % 60:02}' + + +def num(n): + precision = 0 + for prefix in ('', ' K', ' M'): + if n < 1000: + return f'\x02{n:.{precision}f}{prefix}\x02' + n /= 1000 + precision = 1 + return f'\x02{n:.{precision}f} B\x02' + + +@register_command('yt', 'youtube') +def youtube(irc, hostmask, is_admin, args): + video_url = args[1] + if 'v=' in video_url: + video_id = video_url.split('v=', 1)[1] + elif '/shorts/' in video_url: + video_id = video_url.split('/shorts/', 1)[1] + elif video_url.startswith(('youtu.be/', 'https://youtu.be/', + 'http://youtu.be/')): + video_id = video_url.split('youtu.be/', 1)[1] + else: + irc.msg(args[0], 'Usage: .yt <video URL>') + return + video_id = video_id.split('?', 1)[0].split('&', 1)[0].split('#', 1)[0] + res = requests.get('https://returnyoutubedislikeapi.com/votes', + params={'videoId': video_id}) + if res.status_code in (400, 404): + irc.msg(args[0], 'Invalid video ID!') + return + res.raise_for_status() + data = res.json() + + url, md = get_url_and_metadata(irc, args, video_id) + ok = not md or 'error' not in md + + if video_id == 'dQw4w9WgXcQ': + md['title'] = 'Normal video' + md['author'] = 'Definitely not Rick Astley' + + parts = [] + if ok: + parts.append(md['title']) + if md['liveNow']: + parts.append(f'{RED}\x02• Live now\x02') + else: + parts.append(format_length(md['lengthSeconds'])) + parts.append(f'Channel: {md["author"]}') + else: + error_msg = md.get("error", "Unknown error") + if error_msg == "Sign in if you've been granted access to this video": + error_msg = 'The video has been made private' + parts.append(f'Could not fetch video metadata: {error_msg}') + parts.append(f'Views: {num(data["viewCount"])}') + if ok and not md['liveNow']: + parts.append(f'Published {md["publishedText"]}') + parts.append(f'Likes: {GREEN}{num(data["likes"])}') + parts.append(f'Dislikes: {RED}{num(data["dislikes"])}\x03 ' + f'(from returnyoutubedislike.com)') + if ok: + parts.append(f'Link: {url}') + + msg = ' | '.join(part + '\x03' if '\x03' in part else part + for part in parts) + irc.msg(args[0], hostmask[0] + ': ' + msg) |