AiScriptで困ったときに見るメモ

# 前書き
AiScriptは、主に[Misskey](https://github.com/misskey-dev/misskey)のプラグインなどで使用されているスクリプト言語です。

AiScriptは仕様が曖昧な所や思わぬ落とし穴があったりするので参考にしてください。

新しい**知見**を見つけたら #AiScript #MisskeyPlay #MisskeyPlugin で共有しましょう。随時更新します。
連絡先:[`@salano_ym@misskey.io`](https://misskey.io/@salano_ym)

**注意**
- ある程度プログラミングの知識があることを想定しているので、一般的なプログラミングでも起こるような困りごとは載せていません
- AiScriptのバージョンによって情報が変わっている可能性があります
- Misskeyインスタンスのバージョンによって、使える機能やAiScriptのバージョンが違う場合があります
- 公式ドキュメントは微妙に古かったり、そもそも無かったりするので可能なら[GitHubのソース](https://github.com/aiscript-dev/aiscript)も参照してください
# ✔️ リンク集
詳細な解説やリファレンスはここからどうぞ
## 基本情報
- 公式レポジトリ

https://github.com/aiscript-dev/aiscript

- Get started

https://github.com/aiscript-dev/aiscript/blob/master/docs/get-started.md

https://aiscript-dev.github.io/aiscript/

- [構文](https://github.com/aiscript-dev/aiscript/blob/master/docs/syntax.md)

- [標準ライブラリ](https://github.com/aiscript-dev/aiscript/blob/master/docs/std.md)

- [プリミティブプロパティ](https://github.com/aiscript-dev/aiscript/blob/master/docs/primitive-props.md)

## Misskey向け
[Misskey](https://github.com/misskey-dev/misskey)側で実装されているもの。

- AiScriptドキュメント

https://misskey-hub.net/ja/docs/for-developers/aiscript/

- APIリファレンス

https://misskey-hub.net/docs/advanced/plugin-api-reference.html

- [プラグインの作成](https://misskey-hub.net/docs/advanced/create-plugin.html)

Misskey内のスクラッチパッドは`Mk:`や`Ui:`が使えるのでPlayのテスト等におすすめ。(`Plugin:`系は使えないので注意)
`ツール > スクラッチパッド`か`https://SERVER_DOMAIN/scratchpad`で開ける。
- スクラッチパッド
https://misskey-hub.net/ja/mi-web/?path=/scratchpad

## 非公式
- 基本構文

https://qiita.com/YuzuRyo61/items/58d8c2f6a2c2bbfb7eda

- プラグイン解説

https://qiita.com/taichan_/items/0a092bb59b42aeb1f47d

- Misskey Play解説

https://zenn.dev/1step621/articles/a8eb3324ffad6a

https://qiita.com/saki-lere/items/e7f0eb6b7dc0af659a67

- 応用・サンプルコード等

https://misskey.io/play/9nsne9od5uod085p

https://qiita.com/the_quick_fox/items/17d03f2bfc5a56cf73f9

https://balloon-jp.vercel.app/misskey/#aiscript

- ライブラリ
  標準機能に不足しているものを補ってくれる

https://github.com/madorama/aiscript-library

- Misskey Play用デバッガ
実際のMisskey上だと上手く反映されなかったりするときがあるので便利

 https://github.com/gitcrtn/misskey-play-debugger

- [Misskey AiScript部](https://misskey.io/channels/9bhwlim9bz)
Misskey.ioのチャンネル

# ❓ エラーが出たとき
一般的な言語と微妙に違っていて落とし穴が結構多いです

## AiScript本体編
### 0.12.0からの破壊的変更
古いプラグイン等はこれのため正しく動かない。詳細は[CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/master/CHANGELOG.md#0120)を参照。
一部のプラグインは[ここ](https://gist.github.com/saki-lere/8d128649391d59fb0e4c942c5557328e)で移植しました。
|old|0.12.0~|
|-|-|
|`arr[1]`,``arr[arr.len]`` | `arr[0]`,`arr[arr.len-1]`|
|`#a = 1` | `let a = 1`|
|`$a <- 1` | `var a = 1`|
|`yes`,`no`,`_` | `true`,`false`,`null`|
|`a=b`,`a&b`,`a\|b` | `a==b`,`a&&b`,`a\|\|b`|
|`<<` | `return`|
|`? x { a } .? y { b } . { c }` | `if x { a } elif y { b } else { c }`|
|`? x { a => b }` | `match x { a => b }`|
|`~`,`~~` | `for`,`each`|
|`Arr:len(a)`|`a.len`|
|`Str:split(s ',')`|`s.split(',')`|

### AiScriptは空白に厳しい
謎の構文エラーはこれの可能性。

|ERROR|OK|
|-|-|
|`if(true)print(1)` | `if (true) print(1)`|
|`obj . prop` | `obj.prop`|
|`{v:1}` | `{v: 1}`|
|`Obj: type(v)` | `Obj:type(v)`|

### 空のreturnは不可
`return null`等にする。

### データは全部参照
~~`num`型も含めて全て~~参照で、`Obj:copy`はシャロ―コピーなので元データも変わる。

### `Arr:`,`Str:`の関数が無い？
0.12.0からプロパティになった
```javascript
array.push(1)
string.len
```

### `!`の優先順位
`&&`や`||`より低いので適宜かっこをつける。
```typescript
(!a && b) == ( !(a && b) )
(!a && b) != ( (!a) && b )
```

### 配列やオブジェクトの比較は中身を見ない
別々に生成したものは別扱いになるので、内容の一致で比較するときは[各自実装](#配列やオブジェクトを中身で比較)する
```typescript
[1, 2, 3] == [1, 2, 3] // false
{a: 1} == {a: 1} // false
```

### `return`,`break`,`continue`が値として現れるバグ
おそらく内部処理用のラッパーが外れないまま残っている。[issue](https://github.com/syuilo/aiscript/issues/237)
```typescript
eval { return 1 } // == return
```

### 配列の`map`は非同期
評価順が保証されていないので、`print`等を使うと想定通りに動かない場合がある。上手く使えば高速化できるかも
`map`の返値は元と同じ順番
```typescript
Core:range(0, 9).map(@(v) {
  for 100 {}
  print(v) // 2 4 6 8 0 3 7 1 9 5
  return v
})
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

### 複合代入は`+=`,`-=`だけ
`*=`や`/=`等は無い

### 代入は結果を返さない
`a = b`、`a += b`、`a -= b`は常に`null`

## Misskey編

### `Plugin:register_post_form_action`のコールバック
コールバックの第一引数の中身は`text`と`cw`しかないことに注意。
第2引数は、引数を2つ取って次のようにする。[出典](https://misskey.io/notes/9b24pr6xf4)
```typescript
@f(form, update) {
  // form.renote は不可
  let updated_text = doSomething(form.text)
  update("text", updated_text)
  update("cw", "changed!!")
}
```

### 使えない関数・ライブラリがある
Misskey内では場所によって使えるライブラリが違う。[詳細](#使えるライブラリ)

### プラグインでAPIが動かない
権限を取ろう

https://misskey-hub.net/docs/advanced/create-plugin.html#permissions

### `Plugin:register_note_****_interruptor`のバグ？
- `Plugin:register_note_post_interruptor`を使うと投稿時エラーになる。Noteオブジェクトに余計なnullプロパティが入ってしまっているのが原因？[Misskey2024.2.0](https://github.com/misskey-dev/misskey/releases/tag/2024.2.0)で直った？

    ```typescript
    // objのnullなプロパティを再帰的に除去
    @remove_null_property(object) {
      if Core:type(object) != 'obj' {
        return object
      }
      let new_obj = {}
      each let kv, Obj:kvs(object) {
        let v = remove_null_property(kv[1])
        if Core:type(v) != 'null' {
          Obj:set(new_obj, kv[0], v)
        }
      }
      return new_obj
    }

    note = remove_null_property(note)
    ```

## 古い情報

### ~~each文はreturn, breakがcontinueになるバグ~~
[AiScript0.12.3](https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0123)で直した。
```typescript
each let v, [1, 2, 3] {
  if (v==2) break
  print(v) // 1 3
}
```

### ~~`&&`,`||`は短絡評価ではない~~
0.14.1から短絡評価に。
~~`+`,`==`等と同じで全ての被演算子が先に評価される~~
```typescript
@a() {
  print("a")
  return false
}
@b() {
  print("b")
  return true
}
a() && b() 
// a b
```

### ~~配列関数のコールバックの添字~~
配列のmap関数等に渡すコールバックは添字も受け取れる~~が1から始まる~~。 [issue](https://github.com/aiscript-dev/aiscript/issues/235)
[0.13.0](https://github.com/aiscript-dev/aiscript/blob/master/CHANGELOG.md#0130)から0始まりになる。破壊的変更なので注意
```typescript
let arr = [0, 1, 2]
arr.map(@(v, i) { print(i) }) // 1 2 3
arr.find(@(v, i) {
  print(i) // 1 2 3
  false
})
arr.filter(@(v, i) {
  print(i) // 1 2 3
  false
})
arr.reduce(@(a, v, i) { print(i) } []) // 1 2 3
```

### ~~`let`でも再代入できてしまう~~
1.16.0から変更できないようになった

~~`let`は単に意思表示~~
> 仕様上はイミュータブルですが、代入時にいちいちチェックしてるとパフォーマンスが落ちそうにゃので実際はイミュータブルを意図していることを示すものという扱いですね

https://misskey.io/notes/9b4dnwzz5s

### ~~`Plugin:register_note_****_interruptor`のバグ？~~
- ~~`Plugin:register_note_view_interruptor`がノート詳細でしか機能しない。~~[Misskey13.5.2](https://misskey-hub.net/docs/releases.html#_13-5-2)で直った

# ❗ 役立ち情報
ドキュメントが圧倒的に不足していて知らないと分からない機能や、逆にありそうで無い機能が結構多い。

## 基礎
### 拡張子
~~`.is` [出典](https://github.com/aiscript-dev/aiscript/issues/19#issuecomment-611333412)~~
`.ais` [出典](https://github.com/aiscript-dev/aiscript/issues/19#issuecomment-2267328917)

### for文に初期値を設定
`for let i=START, COUNT {}`
```typescript
// 5から始めて3つ分
for let i=5, 3 {
  print(i) // 5 6 7
}
```

### オブジェクトの要素アクセス
要素名が決まってるならドットでいい。
`Obj:get(obj 'value')`のような書き方は回りくどい。
```typescript
let obj = {value: 10}
<: obj.value // 10
```
1.16.0からは`[]`でもアクセス可能
```typescript
let key = 'value'
obj[key]
```

### 型チェック
`Core:type`関数を使う。
```typescript
if (Core:type(value) == 'num') {
  ...
}
```

### match式
```typescript
let v = 2
let a = match (v) {
  1 => 6
  2 => 3
  3 => 54
  * => 100
}
print(a) // 3
```

### 型を書ける
ただしPythonの型ヒントと同じでチェックはしてくれない
組み込みの型以外を書くと`Syntax Error`

```typescript
@func(v: num, p: @(num) => bool): num {
  if p(v) 1 else 2
}
let a: arr<num> = [1, 2, 3]
let o: obj = {a: 1}
```

https://github.com/aiscript-dev/aiscript/issues/143

### `while`
```typescript
// while (pred) { something() }
loop {
  if !(pred) { break }
  something()
}
```

### 文字列連結
`+`は使えない
```typescript
let x = 'abc'
let y = 'def'
<: `{x}{y}` // abcdef
<: [x, y].join() // abcdef
```

## 応用

### 任意の値を文字列化
1.16.0から`Core:to_str`とテンプレート文字列で文字列化可能になった。

~~組み込み機能だと`num`型しか文字列化できないため。~~
関数は全て`@`。[`Json:stringify`](https://github.com/aiscript-dev/aiscript/blob/master/docs/std.md#jsonstringifyv-value-str)もあるが、オブジェクトのキーに引用符が付き、関数は消えてしまうので注意。
```typescript
@to_s(value) {
  match Core:type(value) {
    'str' => `"{value}"`
    'num' => value.to_str()
    'null' => "null"
    'bool' =>  if value "true" else "false"
    'arr' => {
      let s = value.map(to_s).join(",")
      `[{s}]`
    }
    'obj' => {
      let s = Obj:kvs(value).map(@(kv){
        `{kv[0]}:{to_s(kv[1])}`
      }).join(",")
      ['{' s '}'].join('')
    }
    'fn' => "@"
    * => "?"
  }
}
```
```typescript
<: to_s({a: 1, b: ["ff", null, [false, @(a){a+1}]]})
// {a:1,b:["ff",null,[false,@]]}
```

### オブジェクトから特定のキーを削除したコピーを生成
```typescript:1つ削除版
@obj_removed(object: obj, key: str): obj {
  let new_obj = {}
  each let kv, Obj:kvs(object) {
    if kv[0] != key {
      new_obj[kv[0]] = kv[1]
    }
  }
  return new_obj
}
```
```typescript
obj_removed({a: 1, b: 2, c: 3}, "a") // == {b: 2, c: 3}
```
```typescript:複数削除版
@obj_removed_keys(object: obj, keys: arr<str>): obj {
  let new_obj = {}
  each let kv, Obj:kvs(object) {
    if !keys.incl(kv[0]) {
      new_obj[kv[0]] = kv[1]
    }
  }
  return new_obj
}
```
```typescript
obj_removed_keys({a: 1, b: 2, c: 3} ["a", "c"]) // == {b: 2}
```

### 条件分岐テクニック
場合によっては`if-elif-else`より見やすいかも。[出典](https://misskey.io/notes/9b4h8v1e6l)
```typescript
let progress = 50
match true {
  progress >= 100 => print("完了！")
  progress >= 80 => print(`{progress}% あと少し！`)
  progress >= 50 => print(`{progress}% 後半戦！`)
  progress >= 20 => print(`{progress}% がんばってます！`)
  * => print(`{progress}% 開始！`)
}
```

### 配列やオブジェクトを中身で比較
関数は参照一致のみ
```typescript
@eq(a: any, b: any): bool {
  if Core:type(a) != Core:type(b) {
    return false
  }
  match Core:type(a) {
    'arr' => {
      if a.len != b.len {
        return false
      }
      for let i a.len {
        if !eq(a[i] b[i]) {
          return false
        }
      }
      return true
    }
    'obj' => {
      let akv = Obj:kvs(a)
      if akv.len != Obj:keys(b).len {
        return false
      }
      each let kv akv {
        if !Obj:has(b, kv[0]) {
          return false
        }
        if !eq(kv[1], b[kv[0]]) {
          return false
        }
      }
      return true
    }
    * => a == b
  }
}
```
```typescript
<: {a: [1, 2]} == {a: [1, 2]} // false
<: eq({a: [1, 2]} {a: [1, 2]}) // true
```

### 今日の曜日
```typescript
let weekdays = ["日", "月", "火", "水", "木", "金", "土"]
@weekday() {
  var y = Date:year()
  var m = Date:month()
  let d = Date:day()
  if (m < 3) {
    y -= 1
    m += 12
  }
  let w = eval {
    var w = y + Math:floor(y / 4) - Math:floor(y / 100)
    w += Math:floor(y / 400)
    w += Math:floor((13 * m + 8) / 5)
    w = (w + d) % 7
    Math:floor(w)
  }
  return weekdays[w]
}
```

### `splice`
配列`a`の`index`番目から`count`個の要素を削除し、`items`を挿入する
```typescript
@splice(a: arr, index: num, count: num, items: arr) {
  a.slice(0, index).concat(items).concat(a.slice(index + count, a.len))
}
```
```typescript
splice(['a', 'b', 'c', 'd', 'e', 'f'], 2, 3, ['x', 'y'])
// ['a', 'b', 'x', 'y', 'f']
```

## Misskey

### Misskey Playの書き方
ざっくり説明してます。

https://qiita.com/saki-lere/items/e7f0eb6b7dc0af659a67

### 使えるライブラリ

|場所|使えるもの|
|-|-|
|共通|`Mk:`,`USER_ID`,`USER_NAME`,`USER_USERNAME`,`CUSTOM_EMOJIS`,`LOCALE`,`SERVER_URL`|
|スクラッチパッド|`Ui:`|
|Play|`Ui:`,`THIS_URL`,`THIS_ID`|
|プラグイン|`Plugin:`|
|AiScript App(ウィジェット)|`Ui:`|
|AiScriptコンソール(ウィジェット)|-|
|ボタン(ウィジェット)|-|

### APIの権限
|種類|権限名|
|-|-|
|アカウント|`read:account`,`write:account`|
|ブロック|`read:blocks`,`write:blocks`|
|ドライブ|`read:drive`,`write:drive`|
|お気に入り|`read:favorites`,`write:favorites`|
|フォロー|`read:following`,`write:following`|
|チャット|`read:messaging`,`write:messaging`|
|ミュート|`read:mutes`,`write:mutes`|
|ノート投稿|`write:notes`|
|通知|`read:notifications`,`write:notifications`|
|リアクション|`read:reactions`,`write:reactions`|
|投票|`write:votes`|
|ページ|`read:pages`,`write:pages`|
|ページのいいね|`read:page-likes`,`write:page-likes`|
|ユーザーグループ|`read:user-groups`,`write:user-groups`|
|チャンネル|`read:channels`,`write:channels`|
|ギャラリー|`read:gallery`,`write:gallery`|
|ギャラリーのいいね|`read:gallery-likes`,`write:gallery-likes`|

### プラグインのメタデータのプリセット
```typescript
/// @ 0.12.4
### {
  name: "プラグイン名"
  version: "0.1.0"
  author: "書いた人"
  description: "説明"
  permissions: ["write:notes"]
  config: {
    mojiretsu: {
      type: 'string'
      label: '文字列を書く'
      description: '説明'
      default: 'デフォルト'
    }
  }
}
```

### Plugin等で受け取るノートの中身
細かいとこは間違ってるかも
公式APIドキュメント: [Note](https://misskey-hub.net/docs/api/entity/note.html),[User](https://misskey-hub.net/docs/api/entity/user.html)
<details><summary>Note (閲覧時)</summary><div>

```typescript
Note: {
  id: str
  createdAt: str
  userId: str
  user: User
  text: str | null
  cw: str | null
  visibility: 'public' | 'home' | 'followers' | 'specified'
  localOnly: bool
  renoteCount: num
  repliesCount: num
  reactions: { str: num } // 絵文字名: 個数
  reactionEmojis: {} // 不明
  fileIds: str[]
  files: File[]
  replyId: str | null
  renoteId: str | null
  // チャンネル投稿時以外は存在しない
  channelId: str
  channel: {
      id: str
      name: str
  }
}
```
</div></details>
<details><summary>User</summary><div>

```typescript
User: {
  id: str
  name: str
  username: str
  host: str | null
  avatarUrl: str
  avatarBlurhash: str
  isBot: bool
  isCat: bool
  emojis: {} // 不明
  onlineStatus: 'online' | 'active' | 'offline' | 'unknown'
  badgeRoles: [] // 不明
}
```
</div></details>
<details><summary>File</summary><div>

```typescript
File: {
  id: str
  createdAt: str
  name: str
  type: str
  md5: str
  size: num
  isSensitive: bool
  blurhash: str
  properties: {
      width: num
      height: num
  },
  url: str
  thumbnailUrl: str
  comment: str | null
  folderId: str | null
  folder: str | null
  userId: str | null
  user: str | null
}
```
</div></details>

<details><summary>Note (投稿時)</summary><div>

[出典](https://misskey.io/notes/9b53h56yom)
```typescript
Note: {
  text: str
  fileIDs: str[] | null
  replyId: str | null
  renoteId: str | null
  channelId: str | null
  poll: Poll | null
  cw: str | null
  localOnly: bool
  visibility: 'public' | 'home' | 'followers' | 'specified'
  visibleUserIds: str[] | null
}

Poll {
  choices: str[]
  multiple: bool
  expiredAt: num // UNIX時刻のミリ秒。日時指定で期限設定時
  expiredAfter: num // 経過指定で期限設定時
}
```
</div></details>

### ユーザーごとの日替わり乱数
Misskey Play用
```typescript
let seed = `{USER_ID}{Date:year()}{Date:month()}{Date:day()}`
let random = Math:gen_rng(seed)
let i = random(0, some_array.len - 1)
let value = some_array[i]
```

### Pageのデータを取得
Playの容量に入りきらないデータセット等を置いておくことができる。json形式で書いておくと扱いやすい。

https://qiita.com/the_quick_fox/items/17d03f2bfc5a56cf73f9
```typescript
let raw_text = Mk:api("pages/show", {
  name: "play_title"
  username: "your_usename"
}).content[0].text
let json_data = Json:parse(raw_text)
```

~~`Misskey2023.9.2`以降は外部サーバーからも取得可能。
複数サーバーに同じPlayを置いているときに、1か所のPageを更新するだけで全サーバーに追加コンテンツを配信するといったこともできるはず。~~
`Mk:apiExternal`はセキュリティのため`Misskey2023.12.0`で廃止された。
```typescript
Mk:apiExternal('https://misskey.io', 'pages/show', {...})
```