]> PatchyGit - angel.git/commitdiff
Update invidious instance
authorCzar <czar@kalli.st>
Thu, 14 Dec 2023 20:53:51 +0000 (21:53 +0100)
committerCzar <czar@kalli.st>
Thu, 14 Dec 2023 20:53:51 +0000 (21:53 +0100)
Closes #8

main.py

diff --git a/main.py b/main.py
index 25beda8..319dba5 100644 (file)
--- a/main.py
+++ b/main.py
-import requests
-import bs4
-import yt_dlp as youtube_dl
-import random
-import configparser
-import re
-import io
-import os
-import asyncio
-from collections import defaultdict
-from PythonSed import Sed
-from slixmpp import ClientXMPP
-from urllib.parse import urlparse, parse_qs, urlunparse
-from pantomime import normalize_mimetype
-import cgi
-
-sed_parse = re.compile("(?<!\\\\)[/#]")
-sed_cmd = re.compile("^s[/#].*[/#].*[/#]")
-
-parser = "html.parser"
-user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0)"
-" Gecko/20100101 Firefox/10.0"
-accept_lang = "en-US"
-data_limit = 100000000  # 100MB
-
-headers = {
-    "user-agent": user_agent,
-    "Accept-Language": accept_lang,
-    "Cache-Control": "no-cache",
-}
-
-youtube_links = ["www.youtube.com", "m.youtube.com"]
-
-youtube_link = "youtu.be"
-
-ydl = youtube_dl.YoutubeDL()
-
-invidious_instances = ["invidious.snopyta.org"]
-
-block_list = ("localhost", "127.0.0.1", "0.0.0.0")
-
-req_list = ("http://", "https://")
-
-html_files = ("text/html", "application/xhtml+xml")
-
-
-class Lifo(list):
-    """Limited size LIFO array to store messages and urls."""
-
-    def __init__(self, size):
-        """Initialize the LIFO array."""
-        super().__init__()
-        self.size = size
-
-    def add(self, item):
-        """Add an item to the LIFO array."""
-        self.insert(0, item)
-        if len(self) > self.size:
-            self.pop()
-
-
-def get_youtube_title(url):
-    """Get the title of a youtube video."""
-    try:
-        info = ydl.extract_info(url, download=False)
-        return info["title"]
-    except Exception as e:
-        print(e)
-        return None
-
-
-def get_invidious_link(yurl):
-    """Get an invidious link from a youtube link."""
-    video = yurl.split("/")[-1]
-    instance = random.choice(invidious_instances)
-    return f"https://{instance}/watch?v={video}"
-
-
-def get_yurl(path):
-    """Get a youtube link from a path."""
-    yurl = f"https://youtu.be/{path}"
-    return yurl
-
-
-class AngelBot(ClientXMPP):
-    """AngelBot class."""
-
-    messages = defaultdict(
-        lambda: {
-            "messages": Lifo(100),
-            "links": Lifo(10),
-            "previews": Lifo(10),
-        }
-    )
-
-    def get_urls(self, msg):
-        """Get urls from a message."""
-        str_list = msg["body"].strip().split()
-        urls = [u for u in str_list if any(r in u for r in req_list)]
-        return urls
-
-    def send_youtube_info(self, uri, sender, mtype):
-        """Send youtube info to the sender."""
-        yurl = None
-        if uri.netloc == youtube_link:
-            yurl = get_yurl(uri.path)
-        elif "v" in (query := parse_qs(uri.query)):
-            if v := query["v"]:
-                yurl = get_yurl(v[0])
-            else:
-                return
-
-        invidious = get_invidious_link(yurl)
-
-        if output := get_youtube_title(invidious):
-            if output in self.messages[sender]["previews"]:
-                return
-            self.messages[sender]["previews"].add(output)
-
-            self.send_message(mto=sender, mbody=f"*{output}*", mtype=mtype)
-            self.send_message(mto=sender, mbody=invidious, mtype=mtype)
-
-    async def parse_uri(self, uri, sender, mtype):
-        """Parse a uri and send the result to the sender."""
-        netloc = uri.netloc
-        if netloc in (youtube_links + [youtube_link]):
-            self.send_youtube_info(uri, sender, mtype)
-        elif netloc.split(":")[0] in block_list:
-            return
-        else:
-            await self.process_link(uri, sender, mtype)
-
-    async def process_link(self, uri, sender, mtype):
-        """Process a link and send the result to the sender."""
-        url = urlunparse(uri)
-        r = requests.get(url, stream=True, headers=headers, timeout=6)
-        if not r.ok:
-            return
-
-        ftype = normalize_mimetype(r.headers.get("content-type"))
-
-        if not ftype:
-            return
-
-        if ftype in html_files:
-            data = ""
-            for i in r.iter_content(chunk_size=1024, decode_unicode=False):
-                data += i.decode("utf-8", errors="ignore")
-                if len(data) > data_limit or "</head>" in data.lower():
-                    break
-            soup = bs4.BeautifulSoup(data, parser)
-            if title := soup.find("title"):
-                output = title.text.strip()
-                if output:
-                    output = f"*{output}*" if ("\n" not in output) else output
-                    if output in self.messages[sender]["previews"]:
-                        return
-
-                    self.messages[sender]["previews"].add(output)
-                    if r.history:
-                        self.send_message(mto=sender, mbody=r.url, mtype=mtype)
-                    self.send_message(mto=sender, mbody=output, mtype=mtype)
-
-        else:
-            try:
-                lenght = 0
-                outfile = io.BytesIO()
-                for chunk in r.iter_content(
-                    chunk_size=512,
-                    decode_unicode=False,
-                ):
-                    lenght += 512
-                    if lenght >= data_limit:
-                        return
-                    outfile.write(chunk)
-
-                content_disposition = r.headers.get("content-disposition")
-                filename = None
-                if content_disposition:
-                    _, params = cgi.parse_header(content_disposition)
-                    filename = params.get("filename")
-                else:
-                    filename = os.path.basename(uri.path)
-
-                ext = os.path.splitext(filename)[1] if filename else ".txt"
-                fname = filename if filename else f"file{ext}"
-                await self.embed_file(url, sender, mtype, ftype, fname, outfile)
-            except Exception as e:
-                print(e)
-
-    async def embed_file(self, url, sender, mtype, ftype, fname, outfile):
-        """Embed a file and send the result to the sender."""
-        furl = await self.plugin["xep_0363"].upload_file(
-            fname, content_type=ftype, input_file=outfile
-        )
-        message = self.make_message(sender)
-        message["body"] = furl
-        message["type"] = mtype
-        message["oob"]["url"] = furl
-        message.send()
-
-    async def parse_urls(self, msg, urls, sender, mtype):
-        """Parse urls and send the result to the sender."""
-        body = msg["body"].lower()
-        if "nsfw" in body: return
-        if "nsfl" in body: return
-        for u in urls:
-            if u in self.messages[sender]["links"]:
-                continue
-            else:
-                self.messages[sender]["links"].add(u)
-
-            uri = urlparse(u)
-            await self.parse_uri(uri, sender, mtype)
-
-    def sed_command(self, msg, sender, mtype):
-        """Process sed command."""
-        try:
-            text = msg["body"]
-            if not sed_cmd.match(text):
-                self.messages[sender]["messages"].add(text)
-                return
-            sed_args = sed_parse.split(text)
-
-            if len(sed_args) < 4:
-                return
-
-            sed = Sed()
-            sed.load_string(text)
-
-            for message in self.messages[sender]["messages"]:
-                if sed_args[1] not in message:
-                    continue
-                msg = io.StringIO(message)
-                res = "\n".join(sed.apply(msg, None))
-                self.messages[sender]["messages"].add(res)
-                return self.send_message(
-                    mto=sender,
-                    mbody=res,
-                    mtype=mtype,
-                )
-
-        except Exception as e:
-            print(e)
-
-    def __init__(self, jid, password, nick="angel", autojoin=None):
-        """Initialize the bot."""
-        ClientXMPP.__init__(self, jid, password)
-        self.jid = jid
-        self.nick = nick
-        self.autojoin = autojoin or []
-        self.register_plugin("xep_0030")
-        self.register_plugin("xep_0060")
-        self.register_plugin("xep_0054")
-        self.register_plugin("xep_0045")
-        self.register_plugin("xep_0066")
-        self.register_plugin("xep_0084")
-        self.register_plugin("xep_0153")
-        self.register_plugin("xep_0363")
-
-        self.add_event_handler("session_start", self.session_start)
-        self.add_event_handler("message", self.message)
-        self.add_event_handler("groupchat_message", self.muc_message)
-        # self.add_event_handler("vcard_avatar_update", self.debug_event)
-        # self.add_event_handler("stream_error", self.debug_event)
-        self.add_event_handler("disconnected", lambda _: self.connect())
-
-    async def session_start(self, event):
-        """Start the bot."""
-        self.send_presence()
-        await self.get_roster()
-        await self.update_info()
-        for channel in self.autojoin:
-            try:
-                self.plugin["xep_0045"].join_muc(channel, self.nick)
-            except Exception as e:
-                print(e)
-
-    async def update_info(self):
-        """Update the bot info."""
-        with open("angel.png", "rb") as avatar_file:
-            avatar = avatar_file.read()
-
-        avatar_type = "image/png"
-        avatar_id = self.plugin["xep_0084"].generate_id(avatar)
-        avatar_bytes = len(avatar)
-
-        asyncio.gather(self.plugin["xep_0084"].publish_avatar(avatar))
-
-        asyncio.gather(
-            self.plugin["xep_0153"].set_avatar(
-                avatar=avatar,
-                mtype=avatar_type,
-            )
-        )
-
-        info = {
-            "id": avatar_id,
-            "type": avatar_type,
-            "bytes": avatar_bytes,
-        }
-
-        asyncio.gather(self.plugin["xep_0084"].publish_avatar_metadata([info]))
-
-        vcard = self.plugin["xep_0054"].make_vcard()
-
-        vcard["URL"] = "https://wiki.kalli.st/Angel"
-        vcard["DESC"] = "Angel is a bot that can do link previews and embeds."
-        vcard["NICKNAME"] = "Angel"
-        vcard["FN"] = "Angel"
-
-        asyncio.gather(self.plugin["xep_0054"].publish_vcard(vcard))
-
-    async def message(self, msg):
-        """Process a message."""
-        if msg["type"] in ("chat", "normal"):
-            edit = "urn:xmpp:message-correct:0" in str(msg)
-            if edit:
-                return
-
-            mtype = msg["type"]
-            sender = msg["from"].bare
-
-            try:
-                if not msg["oob"]["url"]:
-                    if urls := self.get_urls(msg):
-                        await self.parse_urls(msg, urls, sender, mtype)
-            except Exception as e:
-                print(e)
-
-            self.sed_command(msg, sender, mtype)
-
-    async def muc_message(self, msg):
-        """Process a groupchat message."""
-        if msg["type"] in ("groupchat", "normal"):
-            edit = "urn:xmpp:message-correct:0" in str(msg)
-
-            if edit:
-                return
-
-            if msg["mucnick"] == self.nick:
-                return
-
-            mtype = msg["type"]
-            sender = msg["from"].bare
-
-
-            try:
-                if not msg["oob"]["url"]:
-                    if urls := self.get_urls(msg):
-                        await self.parse_urls(msg, urls, sender, mtype)
-            except Exception as e:
-                print(e)
-
-            self.sed_command(msg, sender, mtype)
-
-
-if __name__ == "__main__":
-    config = configparser.ConfigParser()
-    config.read("config.ini")
-    jid = config["angel"]["jid"]
-    password = config["angel"]["password"]
-    autojoin = config["angel"]["autojoin"].split()
-    nick = config["angel"]["nick"]
-    bot = AngelBot(jid, password, nick=nick, autojoin=autojoin)
-
-    bot.connect()
-    bot.process(forever=True)
+import requests\r
+import bs4\r
+import yt_dlp as youtube_dl\r
+import random\r
+import configparser\r
+import re\r
+import io\r
+import os\r
+import asyncio\r
+from collections import defaultdict\r
+from PythonSed import Sed\r
+from slixmpp import ClientXMPP\r
+from urllib.parse import urlparse, parse_qs, urlunparse\r
+from pantomime import normalize_mimetype\r
+import cgi\r
+\r
+sed_parse = re.compile("(?<!\\\\)[/#]")\r
+sed_cmd = re.compile("^s[/#].*[/#].*[/#]")\r
+\r
+parser = "html.parser"\r
+user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0)"\r
+" Gecko/20100101 Firefox/10.0"\r
+accept_lang = "en-US"\r
+data_limit = 100000000  # 100MB\r
+\r
+headers = {\r
+    "user-agent": user_agent,\r
+    "Accept-Language": accept_lang,\r
+    "Cache-Control": "no-cache",\r
+}\r
+\r
+youtube_links = ["www.youtube.com", "m.youtube.com"]\r
+\r
+youtube_link = "youtu.be"\r
+\r
+ydl = youtube_dl.YoutubeDL()\r
+\r
+invidious_instances = ["invidious.kalli.st"]\r
+\r
+block_list = ("localhost", "127.0.0.1", "0.0.0.0")\r
+\r
+req_list = ("http://", "https://")\r
+\r
+html_files = ("text/html", "application/xhtml+xml")\r
+\r
+\r
+class Lifo(list):\r
+    """Limited size LIFO array to store messages and urls."""\r
+\r
+    def __init__(self, size):\r
+        """Initialize the LIFO array."""\r
+        super().__init__()\r
+        self.size = size\r
+\r
+    def add(self, item):\r
+        """Add an item to the LIFO array."""\r
+        self.insert(0, item)\r
+        if len(self) > self.size:\r
+            self.pop()\r
+\r
+\r
+def get_youtube_title(url):\r
+    """Get the title of a youtube video."""\r
+    try:\r
+        info = ydl.extract_info(url, download=False)\r
+        return info["title"]\r
+    except Exception as e:\r
+        print(e)\r
+        return None\r
+\r
+\r
+def get_invidious_link(yurl):\r
+    """Get an invidious link from a youtube link."""\r
+    video = yurl.split("/")[-1]\r
+    instance = random.choice(invidious_instances)\r
+    return f"https://{instance}/watch?v={video}"\r
+\r
+\r
+def get_yurl(path):\r
+    """Get a youtube link from a path."""\r
+    yurl = f"https://youtu.be/{path}"\r
+    return yurl\r
+\r
+\r
+class AngelBot(ClientXMPP):\r
+    """AngelBot class."""\r
+\r
+    messages = defaultdict(\r
+        lambda: {\r
+            "messages": Lifo(100),\r
+            "links": Lifo(10),\r
+            "previews": Lifo(10),\r
+        }\r
+    )\r
+\r
+    def get_urls(self, msg):\r
+        """Get urls from a message."""\r
+        str_list = msg["body"].strip().split()\r
+        urls = [u for u in str_list if any(r in u for r in req_list)]\r
+        return urls\r
+\r
+    def send_youtube_info(self, uri, sender, mtype):\r
+        """Send youtube info to the sender."""\r
+        yurl = None\r
+        if uri.netloc == youtube_link:\r
+            yurl = get_yurl(uri.path)\r
+        elif "v" in (query := parse_qs(uri.query)):\r
+            if v := query["v"]:\r
+                yurl = get_yurl(v[0])\r
+            else:\r
+                return\r
+\r
+        invidious = get_invidious_link(yurl)\r
+\r
+        if output := get_youtube_title(invidious):\r
+            if output in self.messages[sender]["previews"]:\r
+                return\r
+            self.messages[sender]["previews"].add(output)\r
+\r
+            self.send_message(mto=sender, mbody=f"*{output}*", mtype=mtype)\r
+            self.send_message(mto=sender, mbody=invidious, mtype=mtype)\r
+\r
+    async def parse_uri(self, uri, sender, mtype):\r
+        """Parse a uri and send the result to the sender."""\r
+        netloc = uri.netloc\r
+        if netloc in (youtube_links + [youtube_link]):\r
+            self.send_youtube_info(uri, sender, mtype)\r
+        elif netloc.split(":")[0] in block_list:\r
+            return\r
+        else:\r
+            await self.process_link(uri, sender, mtype)\r
+\r
+    async def process_link(self, uri, sender, mtype):\r
+        """Process a link and send the result to the sender."""\r
+        url = urlunparse(uri)\r
+        r = requests.get(url, stream=True, headers=headers, timeout=6)\r
+        if not r.ok:\r
+            return\r
+\r
+        ftype = normalize_mimetype(r.headers.get("content-type"))\r
+\r
+        if not ftype:\r
+            return\r
+\r
+        if ftype in html_files:\r
+            data = ""\r
+            for i in r.iter_content(chunk_size=1024, decode_unicode=False):\r
+                data += i.decode("utf-8", errors="ignore")\r
+                if len(data) > data_limit or "</head>" in data.lower():\r
+                    break\r
+            soup = bs4.BeautifulSoup(data, parser)\r
+            if title := soup.find("title"):\r
+                output = title.text.strip()\r
+                if output:\r
+                    output = f"*{output}*" if ("\n" not in output) else output\r
+                    if output in self.messages[sender]["previews"]:\r
+                        return\r
+\r
+                    self.messages[sender]["previews"].add(output)\r
+                    if r.history:\r
+                        self.send_message(mto=sender, mbody=r.url, mtype=mtype)\r
+                    self.send_message(mto=sender, mbody=output, mtype=mtype)\r
+\r
+        else:\r
+            try:\r
+                lenght = 0\r
+                outfile = io.BytesIO()\r
+                for chunk in r.iter_content(\r
+                    chunk_size=512,\r
+                    decode_unicode=False,\r
+                ):\r
+                    lenght += 512\r
+                    if lenght >= data_limit:\r
+                        return\r
+                    outfile.write(chunk)\r
+\r
+                content_disposition = r.headers.get("content-disposition")\r
+                filename = None\r
+                if content_disposition:\r
+                    _, params = cgi.parse_header(content_disposition)\r
+                    filename = params.get("filename")\r
+                else:\r
+                    filename = os.path.basename(uri.path)\r
+\r
+                ext = os.path.splitext(filename)[1] if filename else ".txt"\r
+                fname = filename if filename else f"file{ext}"\r
+                await self.embed_file(url, sender, mtype, ftype, fname, outfile)\r
+            except Exception as e:\r
+                print(e)\r
+\r
+    async def embed_file(self, url, sender, mtype, ftype, fname, outfile):\r
+        """Embed a file and send the result to the sender."""\r
+        furl = await self.plugin["xep_0363"].upload_file(\r
+            fname, content_type=ftype, input_file=outfile\r
+        )\r
+        message = self.make_message(sender)\r
+        message["body"] = furl\r
+        message["type"] = mtype\r
+        message["oob"]["url"] = furl\r
+        message.send()\r
+\r
+    async def parse_urls(self, msg, urls, sender, mtype):\r
+        """Parse urls and send the result to the sender."""\r
+        body = msg["body"].lower()\r
+        if "nsfw" in body: return\r
+        if "nsfl" in body: return\r
+        for u in urls:\r
+            if u in self.messages[sender]["links"]:\r
+                continue\r
+            else:\r
+                self.messages[sender]["links"].add(u)\r
+\r
+            uri = urlparse(u)\r
+            await self.parse_uri(uri, sender, mtype)\r
+\r
+    def sed_command(self, msg, sender, mtype):\r
+        """Process sed command."""\r
+        try:\r
+            text = msg["body"]\r
+            if not sed_cmd.match(text):\r
+                self.messages[sender]["messages"].add(text)\r
+                return\r
+            sed_args = sed_parse.split(text)\r
+\r
+            if len(sed_args) < 4:\r
+                return\r
+\r
+            sed = Sed()\r
+            sed.load_string(text)\r
+\r
+            for message in self.messages[sender]["messages"]:\r
+                if sed_args[1] not in message:\r
+                    continue\r
+                msg = io.StringIO(message)\r
+                res = "\n".join(sed.apply(msg, None))\r
+                self.messages[sender]["messages"].add(res)\r
+                return self.send_message(\r
+                    mto=sender,\r
+                    mbody=res,\r
+                    mtype=mtype,\r
+                )\r
+\r
+        except Exception as e:\r
+            print(e)\r
+\r
+    def __init__(self, jid, password, nick="angel", autojoin=None):\r
+        """Initialize the bot."""\r
+        ClientXMPP.__init__(self, jid, password)\r
+        self.jid = jid\r
+        self.nick = nick\r
+        self.autojoin = autojoin or []\r
+        self.register_plugin("xep_0030")\r
+        self.register_plugin("xep_0060")\r
+        self.register_plugin("xep_0054")\r
+        self.register_plugin("xep_0045")\r
+        self.register_plugin("xep_0066")\r
+        self.register_plugin("xep_0084")\r
+        self.register_plugin("xep_0153")\r
+        self.register_plugin("xep_0363")\r
+\r
+        self.add_event_handler("session_start", self.session_start)\r
+        self.add_event_handler("message", self.message)\r
+        self.add_event_handler("groupchat_message", self.muc_message)\r
+        # self.add_event_handler("vcard_avatar_update", self.debug_event)\r
+        # self.add_event_handler("stream_error", self.debug_event)\r
+        self.add_event_handler("disconnected", lambda _: self.connect())\r
+\r
+    async def session_start(self, event):\r
+        """Start the bot."""\r
+        self.send_presence()\r
+        await self.get_roster()\r
+        await self.update_info()\r
+        for channel in self.autojoin:\r
+            try:\r
+                self.plugin["xep_0045"].join_muc(channel, self.nick)\r
+            except Exception as e:\r
+                print(e)\r
+\r
+    async def update_info(self):\r
+        """Update the bot info."""\r
+        with open("angel.png", "rb") as avatar_file:\r
+            avatar = avatar_file.read()\r
+\r
+        avatar_type = "image/png"\r
+        avatar_id = self.plugin["xep_0084"].generate_id(avatar)\r
+        avatar_bytes = len(avatar)\r
+\r
+        asyncio.gather(self.plugin["xep_0084"].publish_avatar(avatar))\r
+\r
+        asyncio.gather(\r
+            self.plugin["xep_0153"].set_avatar(\r
+                avatar=avatar,\r
+                mtype=avatar_type,\r
+            )\r
+        )\r
+\r
+        info = {\r
+            "id": avatar_id,\r
+            "type": avatar_type,\r
+            "bytes": avatar_bytes,\r
+        }\r
+\r
+        asyncio.gather(self.plugin["xep_0084"].publish_avatar_metadata([info]))\r
+\r
+        vcard = self.plugin["xep_0054"].make_vcard()\r
+\r
+        vcard["URL"] = "https://wiki.kalli.st/Angel"\r
+        vcard["DESC"] = "Angel is a bot that can do link previews and embeds."\r
+        vcard["NICKNAME"] = "Angel"\r
+        vcard["FN"] = "Angel"\r
+\r
+        asyncio.gather(self.plugin["xep_0054"].publish_vcard(vcard))\r
+\r
+    async def message(self, msg):\r
+        """Process a message."""\r
+        if msg["type"] in ("chat", "normal"):\r
+            edit = "urn:xmpp:message-correct:0" in str(msg)\r
+            if edit:\r
+                return\r
+\r
+            mtype = msg["type"]\r
+            sender = msg["from"].bare\r
+\r
+            try:\r
+                if not msg["oob"]["url"]:\r
+                    if urls := self.get_urls(msg):\r
+                        await self.parse_urls(msg, urls, sender, mtype)\r
+            except Exception as e:\r
+                print(e)\r
+\r
+            self.sed_command(msg, sender, mtype)\r
+\r
+    async def muc_message(self, msg):\r
+        """Process a groupchat message."""\r
+        if msg["type"] in ("groupchat", "normal"):\r
+            edit = "urn:xmpp:message-correct:0" in str(msg)\r
+\r
+            if edit:\r
+                return\r
+\r
+            if msg["mucnick"] == self.nick:\r
+                return\r
+\r
+            mtype = msg["type"]\r
+            sender = msg["from"].bare\r
+\r
+\r
+            try:\r
+                if not msg["oob"]["url"]:\r
+                    if urls := self.get_urls(msg):\r
+                        await self.parse_urls(msg, urls, sender, mtype)\r
+            except Exception as e:\r
+                print(e)\r
+\r
+            self.sed_command(msg, sender, mtype)\r
+\r
+\r
+if __name__ == "__main__":\r
+    config = configparser.ConfigParser()\r
+    config.read("config.ini")\r
+    jid = config["angel"]["jid"]\r
+    password = config["angel"]["password"]\r
+    autojoin = config["angel"]["autojoin"].split()\r
+    nick = config["angel"]["nick"]\r
+    bot = AngelBot(jid, password, nick=nick, autojoin=autojoin)\r
+\r
+    bot.connect()\r
+    bot.process(forever=True)\r