Open9

AiScriptについて調べる

ツッナツッナ

フォームクリアプラグインを作る

スマホ版のPWAで投稿フォームのテキスト全文を消すのに、フォームクリアのメニューがなかったのでプラグインで追加したく思いAiScriptの調査開始。サンプルのプラグインのコードを少し変更するだけで動作するフォームクリアプラグインは作成できた。

フォームクリアプラグインのソースコード

/// @ 0.12.4
### {
  name: "フォームクリア"
  version: "0.0.1"
  author: "ツッナ"
}

Plugin:register_post_form_action('フォームクリア', @(note, rewrite) {
  rewrite('text', '')
})

このコードを設定のプラグインのインストールのコンソールに貼り付けてインストールすればインストール完了。
設定>プラグイン>プラグインのインストール
コードを貼り付け
インストール完了

情報源など

このページでは投稿フォームにふぐパンチボタンを追加するハンズオンが紹介されている。
ふぐパンチボタンはフォームのテキストエリアの内容を"フグパンチ"で書き換える処理になっているので、"フグパンチ"を空文字にして、ついでに条件分岐もなくしてやれば完成。

なお、私は最初に以下のページのPlugin:register_post_form_action(title, fn)の見本をプラグインフォームに直接貼り付けてインストールしようとしてエラーになった。メタ情報部分の記述は必須の模様。

インストール済みのパネルでプラグイン名等を使用するためだと思われる。
また、AiScriptのメタデータ構文というものらしく、グローバル領域専用で使える模様。

その他気になった点

  • Plugin:register_post_form_action関数の第二引数はコールバック関数(とやら)らしい。
    • コールバック関数の第一引数noteはMisskey側のオブジェクトを受け取っているような挙動
      • noteが具体的にどのようなプロパティ(パブリック変数?)を持っているかはドキュメントの記載を見つけられていない
      • Misskey側のこのあたりのオブジェクトをAiScriptに渡しているんじゃないかと推測している
        • packages/backend/src/models/Note.ts
        • どうやってそのオブジェクトやらパラメータをAiScriptに渡しているのかはわからない。理由は私の知見不足。
        • そのうえTypeScript未経験なので余計になんもわからん。
      • rewrite関数も同様にMisskey側に定義されているのではないかと推測しています
      • このあたりも調査してコードレベルで記事にしたい所存
  • MisskeyのAiScript独自定義パラメータ等は以下に条件文として定義されているのでこのあたりな気がする
  • PCのブラウザ側からプラグインをインストールしたところスマホのPWAアプリ側ではプラグインが有効にはなっていなかった。次の項目については調査が必要。
    • UIの設定を保存して読み込めば共通化されるのか
    • ドライブにテキストを保存してコードを貼り付けるのか
    • Misskey API等を効果的に使えば楽にインストール・配布ができるのか
ツッナツッナ

AiScript関連の情報

Misskey側

AiScript側

  • AiScript(言語自体)のリポジトリ
  • AiScriptのドキュメントディレクトリ
    • https://github.com/aiscript-dev/aiscript/tree/master/docs
    • builtin-props.md:組み込みプロパティ(個人的には標準関数的な解釈。変数じゃなく処理も持っているので)
      • ここにlenやto_strのような関数の記述がある
    • get-started.md:スタートガイド/基本的文法など
      • ここをざっと見れば制御構文、変数宣言の記法等をざっくり把握できる
    • 詳細な文法等(個人的に把握しておきたい部分)
      • literals.md:変数等の宣言、定義詳細について。
      • syntax.md:具体例を上げた詳細な記法説明。
      • std.md:標準関数。入出力や日付、システム系の関数がある認識。
ツッナツッナ

VSCodeでAiScriptをシンタックスハイライトしたい

VSCodeのMarketplaceにはaiscript-devから配布されているシンタックスハイライト等開発補助プラグインはなさそうな感じ。
なので公式のリポジトリaiscript-dev/aiscript-vscodeHow-to-Installに従って、release pageから.vsixファイルをダウンロードして、vscodeに直接拡張機能をインストールする。

1. release pageの[Assets]の部分をクリックして開いて、aiscript-vscode-x.x.xx.vsixをダウンロードして任意の場所に保存。

2. vscode上で[Extensions]→[...]→[Install from VSIX...]を選択して、ダウンロードした.vsixファイルを選択する

デバッグはMisskey本体のスクラッチパッドから。本来はローカルに鯖立てするか、コンソール実行可能なように環境を整備したほうが良いのだろうけれど、今回は面倒なのでスキップ。

ツッナツッナ

AiScriptで@Str:Concat(str1, str2)とかstr1.concat(str2)、あるいはstr1 + str2みたいなことできへんかなぁとMisskeyでぼやいていたら有識者の方にご教示いただいた。ありがとうございます。原文ママ転記します。

var new_str = `{str_a}普通の文字列{str_b}`

参考:https://misskey.io/notes/a02xidkgum5v0awn

ツッナツッナ

Misskey用 突然の死ジェネレータプラグイン

いったん突然の死ジェネレータプラグインが完成した。普通に使う分には問題なさそう。
半角英数記号と半角カナの考慮も少し入れているけれど細かい部分はそこまでちゃんと作れていないと思うので、大まかに囲った後は手で文字を修正してくださいな。

実装

/// @ 0.16.0
### {
    name: "突然の死"
    version: "0.0.1"
    author: "ツッナ"
    description: "フォームに入力済みの文字列を突然の死フォーマットで囲います。"
}

/*
    引数`form_text`に渡された文字列の周囲を突然の死の記号で囲った文字列を返す関数。

    form_text: str
        記号で囲う対象文字列。フォームのテキストエリアに入力済みの文字が渡されることを想定。

    return: str
        突然の死のフォーマットで囲った文字列。Str:lfで改行し複数行の文字列を返す。
 */
@surround_with_symbols(form_text) {
    var contents = []

    // 複数行ある場合に長い方の文字列長を選択するための処理
    let lines = form_text.split(Str:lf)
    var num_of_brackets = -1
    each let line, lines {
        let current = get_num_of_brackets(line)
        num_of_brackets = Math:max(num_of_brackets, current)
        contents.push(`> {line} <`)
    }

    // 突然の死の囲い記号を長い文字列長基準で構成する
    var header = `_人{["人"].repeat(num_of_brackets).join()}人_`
    var footer = ` ̄Y^{["Y^"].repeat(num_of_brackets).join()}Y ̄`
    contents.unshift(header)
    contents.push(footer)

    return contents.join(Str:lf)
}

/*
    フォームに入力済みの文字列長を計測して突然の死の囲い記号の幅を返す関数。

    input_line: str
        入力フォームに記入済みの1行文の文字列。
    half_char_width: num(浮動小数点数値)
        半角英数記号と半角カナのマルチバイト文字に対する文字幅。
        初期値0.5で2文字でマルチバイト文字1文字分とする。

    return: num(整数値)
        入力文字から得られた突然の死の囲い記号の必要数。
 */
@get_num_of_brackets(input_line) {
    // strの文字列を10進数でutf-16コード化
    let charcodes = input_line.to_charcode_arr()

    var total_len = 0.0
    each let c, charcodes {
        if (is_multibyte_char(c)) {
            total_len += 1.0
        } else {
            total_len += 0.5
        }
    }

    return Math:trunc(total_len)
}

/*
    引数に与えられた10進数表記のutf-16の文字コードが半角英数記号または半角カナの範囲かを判定する関数。

    charcode: num
        事前に`str.to_charcode_arr()`で10進数値化されたutf-16文字コードの数値配列の1要素。
    
    return: bool
        半角英数記号または半角カナに適合した場合falseを返す。
        それ以外の場合をすべてマルチバイト幅文字と判定してtrueを返す。
 */
@is_multibyte_char(charcode) {
    // 半角文字判定
    if (charcode >= 33 && charcode <= 126) { return false }      // 半角英数字の範囲(Unicode: 0x21 から 0x7E)
    if (charcode >= 65377 && charcode <= 65439) { return false } // 半角カナの範囲(Unicode: U+FF61 から U+FF9F)

    // 上記以外(マルチバイト幅の文字)の場合trueを返す
    return true
}

Plugin:register_post_form_action('突然の死', @(note, rewrite) {
    rewrite('text', surround_with_symbols(note.text))
})

気になったポイント

  • 関数の引数初期値代入構文が使えない?
    • 半角文字の加算値を0.5を初期値に持った引数にしようと思ったのですけれど、スクラッチパッドとプラグインではうまく動かせなかった。
      • 簡単な初期値構文を持つスクリプトを書いて要検証。
  • メタデータ###{}内のdescriptionを改行させることはできない?
    • descriptionの説明文を区切りの良いところで改行したかったけれど、Str:lfを`で囲ったリテラルも\nも改行として使えなかった。
    • descriptionのパラメータを2つ記述してみたら後の分で上書きされた(そらそう)
    • 回避策はないかもしれないやつ?
  • 1文字単位で文字をutf-16の文字コード(10進数)に変換する関数がなさそう?
    • 個人的な実装の好み的に、@is_multibyte_char()の中でstrをcharcodeに変換する処理を書いたほうが良いかなと思ったけれど、str.to_charcode_arr()までしか見つけられなかったので@get_num_of_brackets()でコード化する実装になった。
    • 単一文字でコードに変換したい、みたいのは実装で回避できる問題だから不要って判断なのかもしれない。
ツッナツッナ

配布方法

プラグイン・テーマを配布する でリソース配布用API(URL?)を作成する方法が紹介されている。

実際この方法をするなら以下記事を参考にしてドライブにファイルをアップロードしてPlayとかからインストールボタンを作成するのが一番手軽そうではある。

MisskeyのPluginやThemeをMisskeyの機能のみを活用して配布する #misskey - Qiita

ただそこまでする必要もないかなとも思うので、

  • zennの説明兼配布用記事を作成する
  • Misskey Pagesを作成して共有する

の2つで、Misskeyでノートを共有投稿までにしようと思う。

ツッナツッナ

ノートの詳細メニューからページに直接ノートを埋め込むプラグインをつくる

ノートの詳細メニューにプラグインを追加するコード。

参考:Plugin:register_note_action(title, fn)

Plugin:register_note_action('このノートをページに追加する', @(note) {
  let all_pages = Mk:api('users/pages', {
    userId: USER_ID
  })
})

コード

/// @ 0.16.0
### {
    name: "ノートをページに追加する。",
    version: "0.0.1",
    author: "ツッナ",
    description: "選択中のノートを指定のページに追加します。追加先のページは事前に設定する必要があります。",
    permissions: ["read:pages","write:pages"],
    config: {
        page_ids: {
            type: "string",
            label: "ページURL(@以降)のリスト(カンマ(,)区切り)",
            description: "ノートの詳細メニューから追加したいページのリストをカンマ区切りで指定します。プラグインインストール時にログにページURLの@以降の値とページタイトルの一覧表を出力しています。",
            default: "0000000000000,9999999999999"
        },
        is_desc: {
            type: "boolean",
            label: "先頭からノートを追加する",
            description: "有効化するとページの先頭からノートを追加します。デフォルトではページの末尾にノートを追加します。",
            default: false
        }
    }
}

/**
 * APIリクエストの応答結果を判定する関数。
 * @param {obj} - Mk:apiの戻り値(API レスポンス)
 * @return {boolean} - レスポンスが"error"型か否か。"error"型の場合trueを返す。
 */
@is_error(response) {
    return (Core:type(response) == "error")
}

/**
 * 使用するMisskey APIのEndpoint一覧
 * @property pages.show   : ユーザ名とページ名で指定したページ情報を取得する。
 * @property pages.update : 指定したページをパラメータで指定した値で上書きする。`name`パラメータにnon-empty制約あり。
 * @property users.pages  : ユーザが持つすべてのページ情報を取得する。
 */
:: Endpoint {
    let pages = {
        show: 'pages/show',
        update: 'pages/update'
    }
    let users = {
        pages: 'users/pages'
    }

    let get = {
        page: @(name) { retrieve_page(name) }
        pages: @() { retrieve_pages() }
    }

    let update = {
        page: @(updated_page) { update_pages(updated_page) }
    }
    
    /**
     * 指定のページを取得する関数。
     * @param {string} page_name - ページ名(編集画面で"ページURL"項目の@以降の部分)
     * @return {obj} - pageのjsonオブジェクト。詳細なデータ構造(レスポンス)は以下URL参照。
     *     Misskey.io API doc: https://misskey.io/api-doc#tag/pages/operation/pages___show
     */
    @retrieve_page(page_name) {
        let page = Mk:api(Endpoint:pages.show, {
                name: page_name,
                username: USER_USERNAME
        })
        return page
    }

    /**
     * ユーザの全てのページを取得する関数。
     * 初期設定簡略化のためにコンソールログにページ名とページタイトルを出力するために使用。
     * @return {arr<obj>} - ユーザが作成した全てのページのページオブジェクトリスト。
     */
    @retrieve_pages() {
        return Mk:api(Endpoint:users.pages, { userId: USER_ID })
    }

    /**
     * Misskey API経由でページの更新処理をリクエストする関数。
     * @param {obj} updated_page - ユーザが選択したノートのidを追加済みのページオブジェクト。
     * @return {(error | null)} - ページの更新処理結果。
     *    更新パラメータ及びエラーのデータ構造の詳細は以下URL参照。
     *    Misskey.io API doc: https://misskey.io/api-doc#tag/pages/operation/pages___update
     */
    @update_pages(updated_page) {
        let result = Mk:api(Endpoint:pages.update, {
            pageId: updated_page.id,
            title: updated_page.title,
            name: updated_page.name,
            content: updated_page.content,
            variables: updated_page.variables,
            script: updated_page.script
        })
        return result
    }
}

:: AddNoteToPages {
    /**
     * ページ(オブジェクト)に選択したノートのIDを追加する関数。
     * @param {obj} page  - ユーザがコンテキストメニューから選択した追加先ページのオブジェクト。
     * @param {obj} note - ユーザがコンテキストメニューを表示したノートオブジェクト。
     * @returns {obj} - page.contentパラメータの末尾(または先頭)にノートのidを追加したページオブジェクト。
     *    [設定]から`is_desc`パラメータをtrueにすると、先頭にノートを追加する処理になる。
     */
    @insert(note, page) {
        // ディープコピーを想定(詳細な仕様は要確認)
        let updated_page = Obj:copy(page)
        let current_note = {
            id: Util:uuid(),
            note: note.id,
            type: 'note',
        }
        if (Plugin:config.is_desc) {
            updated_page.content.unshift(current_note)
        } else {
            updated_page.content.push(current_note)
        }

        return updated_page
    }

    /**
     * ユーザのページ一覧を取得してコンソール出力する関数。
     * ユーザがコンソールのログをコピー&ペーストして楽に設定できるよう作成。
     */
    @describe() {
        print(`====== あなたのページ一覧 ======`)
        let pages = Endpoint:get.pages()
        let sample_setting = []
        print(`|   ページ名   |   設定に追記する値   |`)

        each let page, pages {
            print(`|  {page.title}  |  {page.name}  |`)
            sample_setting.push(page.name)
        }
        print("")
        print(`※すべてのページを追加先に登録したい場合、{Str:lf} 以下の行を[設定]の[ページURL]にコピー&ペーストしてください。`)
        print("↓↓↓↓↓↓↓↓↓↓↓")
        print(sample_setting.join(','))
        print("↑↑↑↑↑↑↑↑↑↑↑")
        
        print(`{Str:lf}====== Error log ======{Str:lf}`)
    }

    /**
     * 現在のノートをコンテキストメニューで指定したページに追加する関数。
     * このプラグインのメイン部分。
     * @param note {obj} - メニューを開いた現在のノートのオブジェクト。
     * @param page {obj} - 追加先のページのオブジェクト。
     *    プラグインインストール時に取得したノートオブジェクトを使用して更新する。
     *    インストール後にページのURLが変更された場合エラーになる。
     */
    @commit(note, page) {
        let updated_page = AddNoteToPages:insert(note, page)
        let result = Endpoint:update.page(updated_page)

        if (is_error(result)) {
            Mk:dialog(
                "error",
                `「{updated_page.title}」にノートを追加できませんでした。`,
                "error"
            )
        } else {
            Mk:dialog(
                "info",
                `「{updated_page.title}」にノートを追加しました。`,
                "info"
            )
        }
    }

    /**
     * プラグインインストール時、および[設定]更新時に実行される処理。
     * ノートの詳細メニューへの登録に加えて、
     * プラグインのログにページ一覧の(タイトル,設定値)の対応表を出力する。
     */
    @install() {
        AddNoteToPages:describe()
        each let page_name, Plugin:config.page_ids.split(",") {
            let page = Endpoint:get.page(page_name)
            if (is_error(page)) {
                print(`[https://misskey.io/@{page_name}] を取得できませんでした。{Str:lf}設定がページURLの'@'以降の値と一致しているか確認してください。`)
                continue
            }

            let menu_name = `Add note to:{page.title}`
            Plugin:register_note_action(menu_name, @(note) {
                AddNoteToPages:commit(note, page)
            })
        }
    }
}

AddNoteToPages:install()
ツッナツッナ

Debug:: 的なやつがほしい

Mk:apiを使って開発しているとき、レスポンス(正確には戻り値)がobjarr<obj>orerror<null>型で、printデバッグするのが結構手間だったので、

  • Debug:print(response)で、obj,arr,errorをよしなに捌いてコンソール出力できる関数
  • Debug:analyze(variable)で型や変数の構造を出力してくれる関数
    • Misskey APIとスクラッチパッドを行き来するのが手間なので結果からそのあたりを確認したい
    • ただこれは、プロパティとかを全部出力しないといけないけれど、オブジェクトの持っているプロパティ全部にアクセスする関数は言語レベルでサポートされていないと結構難しいかもしれない?
  • Debug:renderで、Ui:*系のウィジェットを使って、実際の改行などをスクラッチパッドに出力できるようにする関数。
    • あるいはそもそもデバッグ系の出力は全部この形式のほうが良いのかもしれない。

printconsoleにするかもしれないし、Debugも`Scratch`にするかもしれない。名前は要検討だけれど、とりあえずこのあたりの開発者向け関数を作りたいのきもち。

もしかしたら、一回オブジェクトをstringfly?とかの関数で文字列化すると、全部辿れるようになるのかも知らんと思った。