LoginSignup
31
19

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

Last updated at Posted at 2023-02-10

前書き

AiScriptは、主にMisskeyのプラグインなどで使用されているスクリプト言語です。

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

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

注意

  • ある程度プログラミングの知識があることを想定しているので、一般的なプログラミングでも起こるような困りごとは載せていません
  • AiScriptのバージョンによって情報が変わっている可能性があります
  • Misskeyインスタンスのバージョンによって、使える機能やAiScriptのバージョンが違う場合があります
  • 公式ドキュメントは微妙に古かったり、そもそも無かったりするので可能ならGitHubのソースも参照してください

✔️ リンク集

詳細な解説やリファレンスはここからどうぞ

基本情報

  • 公式レポジトリ

  • Get started

Misskey向け

Misskey側で実装されているもの。

Misskey内のスクラッチパッドはMk:Ui:が使えるのでPlayのテスト等におすすめ。(Plugin:系は使えないので注意)
ツール > スクラッチパッドhttps://SERVER_DOMAIN/scratchpadで開ける。

非公式

  • 基本構文

  • プラグイン解説

  • Misskey Play解説

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

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

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

❓ エラーが出たとき

一般的な言語と微妙に違っていて落とし穴が結構多いです

AiScript本体編

0.12.0からの破壊的変更

古いプラグイン等はこれのため正しく動かない。詳細はCHANGELOGを参照。
一部のプラグインはここで移植しました。

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からプロパティになった

array.push(1)
string.len

!の優先順位

&&||より低いので適宜かっこをつける。

(!a && b) == ( !(a && b) )
(!a && b) != ( (!a) && b )

配列やオブジェクトの比較は中身を見ない

別々に生成したものは別扱いになるので、内容の一致で比較するときは各自実装する

[1, 2, 3] == [1, 2, 3] // false
{a: 1} == {a: 1} // false

return,break,continueが値として現れるバグ

おそらく内部処理用のラッパーが外れないまま残っている。issue

eval { return 1 } // == return

配列のmapは非同期

評価順が保証されていないので、print等を使うと想定通りに動かない場合がある。上手く使えば高速化できるかも
mapの返値は元と同じ順番

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 = ba += ba -= bは常にnull

Misskey編

Plugin:register_post_form_actionのコールバック

コールバックの第一引数の中身はtextcwしかないことに注意。
第2引数は、引数を2つ取って次のようにする。出典

@f(form, update) {
  // form.renote は不可
  let updated_text = doSomething(form.text)
  update("text", updated_text)
  update("cw", "changed!!")
}

使えない関数・ライブラリがある

Misskey内では場所によって使えるライブラリが違う。詳細

プラグインでAPIが動かない

権限を取ろう

Plugin:register_note_****_interruptorのバグ?

  • Plugin:register_note_post_interruptorを使うと投稿時エラーになる。Noteオブジェクトに余計なnullプロパティが入ってしまっているのが原因?Misskey2024.2.0で直った?

    // 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で直した。

each let v, [1, 2, 3] {
  if (v==2) break
  print(v) // 1 3
}

&&,||は短絡評価ではない

0.14.1から短絡評価に。
+,==等と同じで全ての被演算子が先に評価される

@a() {
  print("a")
  return false
}
@b() {
  print("b")
  return true
}
a() && b() 
// a b

配列関数のコールバックの添字

配列のmap関数等に渡すコールバックは添字も受け取れるが1から始まるissue
0.13.0から0始まりになる。破壊的変更なので注意

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は単に意思表示

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

Plugin:register_note_****_interruptorのバグ?

  • Plugin:register_note_view_interruptorがノート詳細でしか機能しない。Misskey13.5.2で直った

❗ 役立ち情報

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

基礎

拡張子

.is 出典

for文に初期値を設定

for let i=START, COUNT {}

// 5から始めて3つ分
for let i=5, 3 {
  print(i) // 5 6 7
}

オブジェクトの要素アクセス

要素名が決まってるならドットでいい。
Obj:get(obj 'value')のような書き方は回りくどい。

let obj = {value: 10}
<: obj.value // 10

1.16.0からは[]でもアクセス可能

let key = 'value'
obj[key]

型チェック

Core:type関数を使う。

if (Core:type(value) == 'num') {
  ...
}

match式

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

型を書ける

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

@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}

while

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

文字列連結

+は使えない

let x = 'abc'
let y = 'def'
<: `{x}{y}` // abcdef
<: [x, y].join() // abcdef

応用

任意の値を文字列化

1.16.0からCore:to_strとテンプレート文字列で文字列化可能になった。

組み込み機能だとnum型しか文字列化できないため。
関数は全て@Json:stringifyもあるが、オブジェクトのキーに引用符が付き、関数は消えてしまうので注意。

@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' => "@"
    * => "?"
  }
}
<: to_s({a: 1, b: ["ff", null, [false, @(a){a+1}]]})
// {a:1,b:["ff",null,[false,@]]}

オブジェクトから特定のキーを削除したコピーを生成

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
}
obj_removed({a: 1, b: 2, c: 3}, "a") // == {b: 2, c: 3}
複数削除版
@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
}
obj_removed_keys({a: 1, b: 2, c: 3} ["a", "c"]) // == {b: 2}

条件分岐テクニック

場合によってはif-elif-elseより見やすいかも。出典

let progress = 50
match true {
  progress >= 100 => print("完了!")
  progress >= 80 => print(`{progress}% あと少し!`)
  progress >= 50 => print(`{progress}% 後半戦!`)
  progress >= 20 => print(`{progress}% がんばってます!`)
  * => print(`{progress}% 開始!`)
}

配列やオブジェクトを中身で比較

関数は参照一致のみ

@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
  }
}
<: {a: [1, 2]} == {a: [1, 2]} // false
<: eq({a: [1, 2]} {a: [1, 2]}) // true

今日の曜日

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

配列aindex番目からcount個の要素を削除し、itemsを挿入する

@splice(a: arr, index: num, count: num, items: arr) {
  a.slice(0, index).concat(items).concat(a.slice(index + count, a.len))
}
splice(['a', 'b', 'c', 'd', 'e', 'f'], 2, 3, ['x', 'y'])
// ['a', 'b', 'x', 'y', 'f']

Misskey

Misskey Playの書き方

ざっくり説明してます。

使えるライブラリ

場所 使えるもの
共通 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

プラグインのメタデータのプリセット

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

Plugin等で受け取るノートの中身

細かいとこは間違ってるかも
公式APIドキュメント: Note,User

Note (閲覧時)
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
  }
}
User
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: [] // 不明
}
File
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
}
Note (投稿時)

出典

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 // 経過指定で期限設定時
}

ユーザーごとの日替わり乱数

Misskey Play用

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形式で書いておくと扱いやすい。

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で廃止された。

Mk:apiExternal('https://misskey.io', 'pages/show', {...})
31
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
19