前書き
AiScriptは、主にMisskeyのプラグインなどで使用されているスクリプト言語です。
AiScriptは仕様が曖昧な所や思わぬ落とし穴があったりするので参考にしてください。
新しい知見を見つけたら #AiScript #MisskeyPlay #MisskeyPlugin で共有しましょう。随時更新します。
連絡先:@salano_ym@misskey.io
注意
- ある程度プログラミングの知識があることを想定しているので、一般的なプログラミングでも起こるような困りごとは載せていません
- AiScriptのバージョンによって情報が変わっている可能性があります
- Misskeyインスタンスのバージョンによって、使える機能やAiScriptのバージョンが違う場合があります
- 公式ドキュメントは微妙に古かったり、そもそも無かったりするので可能ならGitHubのソースも参照してください
✔️ リンク集
詳細な解説やリファレンスはここからどうぞ
基本情報
- 公式レポジトリ
- Get started
Misskey向け
Misskey側で実装されているもの。
- AiScriptドキュメント
- APIリファレンス
Misskey内のスクラッチパッドはMk:
やUi:
が使えるのでPlayのテスト等におすすめ。(Plugin:
系は使えないので注意)
ツール > スクラッチパッド
かhttps://SERVER_DOMAIN/scratchpad
で開ける。
非公式
- 基本構文
- プラグイン解説
- Misskey Play解説
- 応用・サンプルコード等
- ライブラリ
標準機能に不足しているものを補ってくれる
- Misskey Play用デバッガ
実際のMisskey上だと上手く反映されなかったりするときがあるので便利
-
Misskey AiScript部
Misskey.ioのチャンネル
❓ エラーが出たとき
一般的な言語と微妙に違っていて落とし穴が結構多いです
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 = b
、a += b
、a -= b
は常にnull
Misskey編
Plugin:register_post_form_action
のコールバック
コールバックの第一引数の中身はtext
とcw
しかないことに注意。
第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
でも再代入できてしまう
let
でも再代入できてしまう1.16.0から変更できないようになった
let
は単に意思表示
仕様上はイミュータブルですが、代入時にいちいちチェックしてるとパフォーマンスが落ちそうにゃので実際はイミュータブルを意図していることを示すものという扱いですね
Plugin:register_note_****_interruptor
のバグ?
Plugin:register_note_****_interruptor
のバグ?-
Misskey13.5.2で直ったPlugin:register_note_view_interruptor
がノート詳細でしか機能しない。
❗ 役立ち情報
ドキュメントが圧倒的に不足していて知らないと分からない機能や、逆にありそうで無い機能が結構多い。
基礎
拡張子
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,@]]}
オブジェクトから特定のキーを削除したコピーを生成
@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
配列a
のindex
番目から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', {...})