aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--misc.py20
-rw-r--r--piston.py117
-rw-r--r--spellcheck.py.old (renamed from spellcheck.py)0
-rw-r--r--wild.py9
-rw-r--r--yt.py146
5 files changed, 282 insertions, 10 deletions
diff --git a/misc.py b/misc.py
index ffd0ed4..0447829 100644
--- a/misc.py
+++ b/misc.py
@@ -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
diff --git a/wild.py b/wild.py
index 3fc9540..dea4265 100644
--- a/wild.py
+++ b/wild.py
@@ -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 = {}
diff --git a/yt.py b/yt.py
new file mode 100644
index 0000000..d819e9c
--- /dev/null
+++ b/yt.py
@@ -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)