Function
action
v1.0
Things 3 JSON Import URL Action Button
An action button for converting Markdown-formatted task lists into Things 3 JSON format and generating import URLs. A link will be appended to the message.
Function ID
things_3_json_import_url_action_button
Creator
@jameslong
Downloads
117+

Function Content
python
"""
title: Things 3 JSON Import URL Generator
author: jameslong
license: MIT
version: 1.0
date: 2024-07-31
description: An action button for converting Markdown-formatted task lists into Things 3 JSON format and generating import URLs. A link will be appended to the message.
icon_url: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAAC1ay+zAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAAUhElEQVR4Ae1beZAc1Xn/+prZ2dlTaHWBJcWSOSyDsTAWGJcRBAQkkHB4weCUKwlV2KmEVDllBYfIpRGQxKaSAhsHHzixDQ5QbCgM2OaICcIYCxA3iEMIIaTVtfc9R1/5/d7rnpmdnWMXico/PKmnu1+/9x2/933f+97rXkMOtYShIZs2GRnZeKiUZt0/o1puEtm4MRTDCGfd8XA17L4ntNZmQluo/P93gQyUpbs7tN6PKHNSIJMJTTLJZIwgZnZeZrDNEL+5IHbCsVzH8E0rsGROdGNajc6mL2FBHE8k8KzCmJtsT088cE3XeNyPIKxaJWG5fPGzWudZC8pR77nU8Eno7OsOnmaazhclNE6DqkfBEprFMGERYhmGmCEe0C5nTbyWdFF9kRYIoyqA2QOK0MV5EudeMHpSxL/rkQ3zn2WXclkjEjVPs5IxJrjuhsGPQK0f2056nWknMA4FCXwMCGSCIEo6XFQwi1lU1lc0m3HLftX6GNAXz/jYMMUE7pTFd3MSerkHc5571ebMggOxzDPIVlTE0lVUl25pVj09hn/29aMfM41gq5PqaHezIz4GPDBMywx9zwgCt4IOBKfsjBEMUocaK0hDcSixUTCYdmhYdhgGfhAGnplo7rAKk6N9vj918m8yS3bTZRu5Q4liSeeyKwY5HWXPvWH4Zbup/QQvN5qH8kkvPybj+7ZKdugd8Qtww1B5B3SlNQQ0iMNa4FpqxA2MuiqGJVaiRVKdK6X1yJPFTraBrZ9zUu1Nbnb46Yc3zDsVghTlryVMXQAYXTdnDO/c6wb+zGk54g43O1owDCvh5obDgVd/Ykz1vywBIh5GWQkHhpRSD1b0W4vx3OsrXIwIA2jT8CTVdaJ0nfCXodPUaQSh5zpNHU5havCiR785/xdrM49DhzPop1WLXbU2quz6uHbC0DIvCRVDzAIYgIm9Txledq846WXi5cfRqDgp1CN3yM8qR4tQ28lW8bK9SqbOlReoEElGsJSLcfqFyFre1iyRPVV/3tOtNQOjpfAxxBxEAS8v3tQ+BJ5WDEAeHWn6CMxVDhUcFQkCNMeDrlSvT+RqgZ+HLG3iTu5DUM4hKJoQBbIasgIEqH7d0akLgApgIGJImFIKGiAbuKCfF9uypMmxaBDCDITnyoPm9b4ODLUNYjbPtQ48t3CkEpaSBe4PVTEzQv/IFZtwIZmN2op5Xa3UcQEdQFZltiUAREoFN0JBvxNPdg+NSN+Tb4ocBV8swMVivuTC63gA43qeZ1sC+neDxow7GAA5KLLo86vkqIWwSGUV5E6LDBUA8SDWolYHAN3lyAnPkSMkQYnIExOfHBweltOWpeTKDf8kHR0dmrHSWvehro3kryWQrufMOZ1WNZo0yGHIctvPeuSVwRHp4mxJISkrjOOkq55znv/Rp13c41bPZpV8awNAAdCt0NLmpEJJUiLkd4aFuj1v7JHvr/9bWb16tRQK8EHTRFPyqCQ/93sVbFW3asRiWPkMoTcIZMUK5eryJ1feIsd/PgKelmAaTiK9kPoBgNqlNgBRH6sQJo1kbAEmcIAQSMdbW1o47yo/pC8eDuU1y2qKxwrgmRoYbSFsGUCG9rZWkYSrZYtnYTESXYuatH7RYMZUys+1AdikVApTzW02+qMdzQoAUALPE9u2JZl0YFgmrj+g1Q8kpey1iudhSYCEiLJQJiVj7AKGJCb9PFy3fqkJQAb9eBRCL+GYNoIxROFIK/MKxHFsSUBxEww/SADAURWdhiC4KXfTwFic8TAAlEUsyEfZCADPEjo2ZFedsV/BCnVd8VMTgLidiYQTQ492OggGDO8mAADqFmyf5m/zhyw+kALo4etWZGUBYxFnCcUvFD8AALQArNCZkAEf/dw0bD+XcCgSN2sy+FetcEzrFzNIIv0FO0RRYMDAIyBLgTjlMgCaeKzOvD6Mh0VtoBQtbGpySu6+514E3YICntFe8YIMlIVRSslGZAAS47VjWQqAegrWBGDbNo0xhHC4AFHaK9pB2AxmSjjc66IjD28P50GFLNOSvv5++bv1/yCXX/YFuePnd8VMY9ZKljRURR7A+U4BAARgokFyeuOZd7CdGqUb9T1AFQDYHAlYGFA3wtAz0gCc6MfK1qBwSNVUnjy2v71DLvvyV+SlZ5F0LTpOPnXiCYquUhRXPLNdGqGasqktQoyXCasNQrgvyrZtPXFz3k4rNS0gbmWHvs2Nh7hwGkwj/STTOKrUpB53msOZ9HmQ/jPPPifHHL1G8oUsBiAnz//qTvn0SavVcwY78kVTtLUgEzakeKNq8Ys6Wq9mzdGsXkqaVX+OYTcRA9BMTQPaz5oc7esKgcOoPRWgb/N45NHfyClrTpbP/eEaeeOFLfLU73+NxOtE5eeqDeWNeBMsyqRjAKojf8U+jLbw2vqria2q6v2vb1bkw9Bs4tSj+EEwJh5FAKr2nFkZj+rMJ6WaWHnWMNide87Zsu6PL5LfPfaQPPo/j8lnT12jFKSypaJsABZgSAqrJ8pGcAgMAjfc13//QTBmAldKGNE0x5UW9p7KAIidIG4988xRiUc1HqHKVnEb1v/wtv9Uwe78iy6TR391H8D4bzn7rDOLbjG9r+avLABuSdkoIw7MARb6WHpBNL3TtLtyOKc9EFmr7uHpSYKOhAeBhT7nIwGiCxD9+oWjSuEOHDgoU1NZdV0JAu/Zxvd9ufFfvyNfvepKueDiy+WX9/XITd/9d7ms+5I6TLQMlMVhqhZ6ampmIGSdZSOJR4mtuRqhOgDo5phammhV2qwIgKcyr+LeXOyIFdRjk35r+w455Zxu+ftrN2I6G5gGQqx8Pl+QzPXfkmvWb5DzL75CHtz6ulz9tfXy1391laIa06pgUbylLMwGuTbRHoDBAgBwXTULxINZ7FB20RAAEInMSEdYH/k3mcW+WM0OYoGHR0bk2E9dKB3ppDzy5DOy7sIvyYsvv6L6xtYxlc3KN755ndyw6T/kvAvPl/f2HZS1q1ZIZsM1KsMrd48yudVlzJuyJBxHPFgRR0o5Bh6aQeM8oCEAmO2RCCkDUOj6cIEkAFDBhmLEUiiR9A+fUcFO7BW8tOUu2T08KUcumCfz2tOy+sTz5N77HlD9qfw1sIyb73wQyq+RXD4vr+4akB9859syb15nlaBXxoSXEW8FALLBALKVy4oM9RASoYhXAPciUVVwDoCywzSYgaFOiUH45AnHy5b7b5cvXnm1NGGu/tNLz5IvXHyZfPd7N8vA0JB87+5H5I8++wk1oT/+0PPy2P/ejrl/ZWPli7z11JnAoDCOKFAQAygz9i8OfRawjDChUAVBnrHtDHOLLaD+LEAQaMJU6P47b5PO9nbZtfeAXHLFFXLjbf8l9z/6hJx36iplDb/e+qb84LZNcuYZpyvraQRwUX9cqCAImSibkpUoqP+HsBaIGSD/VRbAl0Bc+nKq4eqv6AJxwxpnKkIQln7kKPnZj26WTx73MXntnffklE+slMXzOxWd/f2D8jfd6+QvvvylGlQaVSPiQ6Y4Xqi8DQBguBq6QG07Pl0zxUJPZ1PRYKvghTmWSM+2xCB0zZ8vt/zbv8g5nztZXnhrl6SbUwA0lEQiKdeu/xrOTlGJ2dJmOw6GXjnC9HnPtSAfhGE0C/Cmeqm9GIraQz5usRYLBeYOTGkaLD6qexGD0Ibtq29dv1Fab7xJ7n34cdk9OiWP/eQmWbx4oVJ+LqYfMyQANmYBylYsGgks2+qX2gA8oTti7wH7TgRTg+r52AxpwlYYTQD1QLs+h7KnMQipVJNk/nG9cqUlixepnD+eFsuaN7yMedM1HQDgZrkTVJK1IQE0qA1A1Ns2Qp/bIXCxkGeuM7gDowCYve5FWWIQaEUbvvF1FfCKD+d6EfGnLGpXCMaKoK1lBS3IXHdHmOwaAoD3/xNaLoRDKN/c2oW3NuTMuV4/mesvQeCIc6eHhdcK0LkSUpbJQYELgFRz64LYHlU2FPiFwUYkawdB2az6Duzb1+d7zNexuMCrp/aFR0v7ks9gQYS8G0AQAwLBg1t1PHjdqJQrXH5dq18lfcUTjSkDZWlb/BklG2UEwIbnunKw92C/pre5Ftnay+HN29YqNX776v6B8eEhLCxsg/4/D/the5vPlFd2huJ6ytwweozEXDDpg9csswFCt6z9G9OopM97mLgU3FDJQpkoG9Nhy7KMMSRZT73cP0zKm7f11xySOi6A/TAUJ5Ec2fVOrxx3/HLTwqcoFj6JGcibcs0jIifNz8uyTlcJkkLO1YZXqAvaTVm6wJRleCmjLASsY0AUwTn8UPm474EhX3r7fdk/FMjwZCgTuVByeBWweziQF4dE5iewIMKnQ/hiBBunOeO9XQekoy05NkJ+q7pDbu9VK7UBWPW66nTM/HBkZDzv7nxzl7Pi2KWhnWg2TKy6ltqB7BwVvJPz1EhjIIRHAUczTOGMxaF8dV1SlnSpt2pFRaoJUa0uVn5ozJM7Nufl4Z2B9BdU5JEkRp7pKYNyCzRY2syX9EjOsPhz85Phzrf2GhNZL7+o3RtVAMimaixUXWSs1Z5z2jPCJRc8OL+pfdH2XMHvbLMnwuUrlxhtRyzER3EpjDwmIrYqdof2kNxAtvj2qCsfT+fk5iu7pBlvqDhHl9oVO1S9ABXlTrm8L9f+fEQe73fk+A79EgZaKrNgGxYVd/DjYd9wbPCgvPfuwXAqbDWwZzE4NbrrmL33XYxAqHXRPab/1raAqJ058Pak39o1lkwmOscm3PCZLS8Y81osmb+gS9LtHZJINuEDBQd+h11ozMV2oglfbaTk6M6kvNE3JQMjrixdBDZKBhCdDQqcztFuYMSXrX2erO5Kw9xDbI7i4wwcvoszglzgu1LACnIShj6EvYbRKexWtRwRJtNwBs8dd4YfmJyu7sy72YhjLL/86ZfESp4Q5Pp8ccetfHZcsnhBgW+zosDHVJRTkSmppqR0dLbLkmXLxWpfIBcs2SnLF/h8vaHaThdBWxDNHQipR/qaFoBvEAZseXDfSgknRxDR35ORoWHsLOUQfD2s/PCOCl1oWZxWMUCSbGrF4r3Vt1JdFr4WeeWd208+sUhYUZ/5U98C4s/cJBjlNjOF42spG1tirTb3STR+/FXPoES+gGDV2ye7dvbKR1cdK6/lJmQSEdl00uwMl9FKs28c4ChW+VSoaPlZ2TXSIfv2+rL79TfEA3+uQpnzO04C64ayPuigshSc8eICDzi7B2MUC4KRkUaXjCpKfQAu7SElH1QGo9xfESLySuNo1MChSJZpaaopITbS5Rd39MpZy9LC/B9vq4sWwO4KNNWrJBtlZ+EZWInpTcnWN0bkD9IpBDmdeKlnxXalvrEEkAUoMNESNQUi/pVYafLTfusDINGGeuhhogGdEr9pRCpvAiJE6AAGX2Eb3J6PJGQqzcLfksLVCBvi8tMbjDjTLUVT9YxVVTczfjR99PBdDUD0im9Gw6iiAQC6VeB5Q2bZyroc0rriAAjXha/irKxymiXSMjX9OF+IhSQwegseDbCXwMK2MWBxu5nnki1iCaMBmNloWs2sAMDXV/Cn0ijFgseUqoGg2kBxbqHh5ZJKisr9nH21QiW6MQ/W8JW4D+dTmhO46LLIs5KpYshKHljC+viUdRaFhlq74NNzPgy97Bg0Ie1KtrX7Rk8QrNE1HkWOOl5r88CsgYxV7eRwN4eBlYsjdWDDRa0aI2XmxJRAIICEXl4v4iIdagk6Kwtwc2Pjdis/QVEw16I1rV4hByXzcAEXc3ZoOGozVcnHlqrBtC4R2khqMPIeplkXizCyjOPG9NbV7+BsDADi5yeLf0dQvaWurQ/ARvDOYATd8Qn1ESKIa7krTKEKLmyH95Uy7LXK8OgBaWnKipdMYhrDulVbKVqwVaQgLqkod3ZVcpPNyWB+EdLqnJrv0VC11ec6vxAt9PEZvzs5KxeoD0Bke/7UwAg+SWU4p/QQtTiONSXhPlorPqUdsRfKa9lFkhp8F4AgY8d3hhqBsq6xjRMETNuFoEWyqePQF7l+uB3Wg0k/ioCVcURTIU1VeGEGXjb0Jg9g5kLJKJTVZbWf+gAoZcUceibT23LkKX2mk1gcmJCEk3Kjghb0fAOxo5BcJJOCFQuts0Eo17aFXSckOxL0RTaiIa/NkuJokTjlIgvs63/97l5UwgZLf95TrX/9IMge3UziZa+X7X+OcxnCF98/zapQGebsNkadS9UEjiQ+sKp30ErY1kI7z2u4oxXJEVmAYfATEXGnIOvwq3vwxx4alTrSNrAA9OxRA5kb2/O7n3aYp5+LPxfAyh+JeoPtNM79qaQte/fsR9qKT1hasGhCUqO8p45AKhJgChzFt8j79/apt0nTdnur9lUWgHW57xTG9wTjvU/+FM1yPfdA9gYQNAaAbnDSD4OJ57/yWyfVuhF/ofHPpp3CFznMUDC0cTxUjKZz42hgGW288uq7kgYYnOqq+jC6sacmxkAYyARCjvoOUYFW7se6ZREHRE68wWYktd2pQckNbN84+uKtT0BmxqxicCi2r7jQu5IVlTNu9/8yWL72z4MDv791u2+n38FfzKyEIl38myE9pMrU6OFlB8caf1YEnfGVleEHgVHwfMONDs8Liteqzo/vA8NHJEzw789AT0VFRsbiQWX1PTRHxPdM/MmO4U4c3D6x77mNoy/fetfytZnRkS1fx/ZJ40I4Z1uMlSuvTuzYcUsLOnw0fewVpydbl5xqOc0r4BbtGAiAyRRH/fkAmgB9ZR1KEd43sP5YFD1o1FEXdQ9rYwXrsB7lkONP55Dvj/iFyZ35sT1PT75112Y83LnyvKsndjx0C5VvOPpooyjyPPvSfY8lW3oS0tvD1QGWedKJA2tdfj6pl0A4l5dYk/K6uV6XK8NrTjCMkNzw4K7XuBzVnZNTuwvSc+msgzT6vQ8A2IslkzFlMxTux5F/BuE+W1LUy5eue/fo9uoXoWN52e20y7IH5f3tJNLCiB6v42LzI/41oXQBjLU4MhmC8mH5EIEPEZgbAv8HHGthZ7epXi8AAAAASUVORK5CYII=
required_open_webui_version: 0.3.9
"""

from pydantic import BaseModel, Field
from typing import Optional
import json
import urllib.parse
import re


class Action:
    class Valves(BaseModel):
        pass

    class UserValves(BaseModel):
        show_status: bool = Field(
            default=True, description="Show status of the action."
        )
        pass

    def __init__(self):
        self.valves = self.Valves()
        pass

    def create_things_url(self, markdown_list: str) -> str:
        lines = markdown_list.strip().split("\n")
        items = []
        current_heading = None
        current_items = []
        project_title = "Packing List"  # Default title

        def add_section():
            nonlocal current_heading, current_items
            if current_heading is not None:
                items.append(
                    {"type": "heading", "attributes": {"title": current_heading}}
                )
            items.extend(current_items)
            current_items = []

        checkbox_pattern = re.compile(r"^\s*-\s*\[([ xX])\]\s*(.+)$")

        for i, line in enumerate(lines):
            line = line.strip()
            if i == 0 and line.startswith("# "):
                project_title = line[2:].strip()
            elif line.startswith("##"):
                add_section()
                current_heading = line[2:].strip()
            else:
                checkbox_match = checkbox_pattern.match(line)
                if checkbox_match:
                    status, title = checkbox_match.groups()
                    completed = status.lower() == "x"
                    current_items.append(
                        {
                            "type": "to-do",
                            "attributes": {
                                "title": title.strip(),
                                "completed": completed,
                            },
                        }
                    )
                elif line.startswith("- "):
                    current_items.append(
                        {"type": "to-do", "attributes": {"title": line[2:].strip()}}
                    )

        add_section()  # Add the last section

        things_json = [
            {"type": "project", "attributes": {"title": project_title, "items": items}}
        ]

        json_string = json.dumps(things_json, separators=(",", ":"))
        encoded_json = urllib.parse.quote(json_string)
        things_url = f"things:///json?data={encoded_json}"
        return things_url

    async def action(
        self,
        body: dict,
        __user__=None,
        __event_emitter__=None,
        __event_call__=None,
    ) -> Optional[dict]:
        print(f"action:{__name__}")

        user_valves = __user__.get("valves")
        if not user_valves:
            user_valves = self.UserValves()

        if __event_emitter__:
            last_assistant_message = body["messages"][-1]

            if user_valves.show_status:
                await __event_emitter__(
                    {
                        "type": "status",
                        "data": {
                            "description": "Generating Things 3 URL",
                            "done": False,
                        },
                    }
                )

            try:
                things_url = self.create_things_url(last_assistant_message["content"])

                if user_valves.show_status:
                    await __event_emitter__(
                        {
                            "type": "status",
                            "data": {
                                "description": "Things 3 URL Generated",
                                "done": True,
                            },
                        }
                    )

                # Add a citation with the Things 3 URL
                await __event_emitter__(
                    {
                        "type": "message",
                        "data": {
                            "content": f"\n- [Import to Things 3]({things_url})\n"
                        },
                    }
                )

            except Exception as e:
                print(f"Error generating Things 3 URL: {str(e)}")
                if user_valves.show_status:
                    await __event_emitter__(
                        {
                            "type": "status",
                            "data": {
                                "description": "Error Generating Things 3 URL",
                                "done": True,
                            },
                        }
                    )

                    # Add a citation with the error message
                    await __event_emitter__(
                        {
                            "type": "citation",
                            "data": {
                                "source": {"name": "Error:generating Things 3 URL"},
                                "document": [str(e)],
                                "metadata": [
                                    {"source": "Things 3 JSON Import URL Generator"}
                                ],
                            },
                        }
                    )

        return None