플러그인
플러그인은 AiScript로 쓰는 확장입니다. 노트와 유저에 조작을 더하거나, 게시할 내용을 고쳐 쓰거나, 명령 팔레트에 명령을 추가할 수 있습니다. Misskey 본체의 플러그인과 같은 사고방식이지만, NoteDeck 고유의 API가 더해집니다.
최소 구성
맨 앞에 AiScript 버전과 메타 헤더를 씁니다. 버전 줄은 필수이며, 없으면 플러그인이 실행되지 않고 로그에 version header required라고 남습니다. 실행은 항상 새 쪽 인터프리터로 합니다.
/// @ 1.2.1
### {
name: "샘플"
version: "1.0.0"
author: "당신"
description: "무엇을 하는 플러그인인지"
permissions: []
}메타에서 필수인 것은 name과 version만입니다. config를 쓰면 그 값을 Plugin:config로 읽을 수 있습니다.
등록할 수 있는 훅
Plugin:register_*로 훅을 등록합니다. 플러그인은 불러올 때 한 번 실행되며, 거기서 등록한 함수가 나중에 이벤트마다 호출됩니다.
| 훅 | 호출되는 때 |
|---|---|
register_note_action | 노트 메뉴에서 선택되었을 때 |
register_user_action | 유저 메뉴에서 선택되었을 때 |
register_post_form_action | 글 입력란의 버튼이 눌렸을 때 |
register_note_view_interruptor | 노트를 표시하기 직전 |
register_note_post_interruptor | 노트를 게시하기 직전 |
register_page_view_interruptor | 페이지를 표시하기 직전 |
명령 팔레트의 명령은 훅이 아니라 뒤에서 설명할 Nd:register_command로 더합니다.
interruptor는 동기 실행
*_interruptor는 동기적으로 호출되므로, 안에서 확인 대화 상자를 띄울 수 없습니다. 게시를 막는 용도라면 확인을 거치지 않고 자동으로 처리하는 방식 (CW를 자동으로 붙이기, 공개 범위 낮추기)으로 합니다.
NoteDeck 고유 API
| API | 무엇을 하는가 |
|---|---|
Nd:version | 앱 버전 |
Nd:call(id, params) | capability를 호출합니다. 권한 범위 안에서만 성공합니다. 확인 대화 상자가 취소되면 오류가 되지 않고, Core:type이 "error"인 값이 돌아옵니다 |
Nd:capabilities() | 호출할 수 있는 capability 목록 |
Nd:http(url, options) | 외부로의 HTTP 요청. { status, headers, body }가 돌아옵니다. 통신 두절(이름 풀이 실패・연결 거부・타임아웃)도 오류가 되지 않고, status가 0이며 error에 이유가 들어갑니다 |
Nd:on(event, handler) | 앱 안의 이벤트를 구독합니다 |
Nd:register_command(...) | 명령 팔레트에 명령을 더합니다 |
Plugin:open_url(url) | URL을 기본 브라우저로 엽니다 |
Nd:on으로 구독할 수 있는 이벤트는 노트 도착, 알림 도착, 칼럼 추가와 삭제, 스트리밍 연결 상태, 메모 생성·수정·삭제, 스킬 편집, 테마 적용입니다.
핸들러는 앱 프로세스에서 한 번만 호출됩니다 (창을 여러 개 열어도 이벤트는 대표 창 하나에만 전달됩니다). 노트 표시 훅은 창마다 호출되므로 훅 안에 부작용을 쓰지 마세요.
Nd:register_command로 더한 명령은 명령 팔레트, 키 바인딩, AI의 도구, 로컬 API 어디에서든 호출할 수 있습니다. AI나 로컬 API에서 호출되었을 때의 실효 권한은 "호출한 쪽의 권한 ∩ 플러그인의 권한"입니다.
호출이 실패했을 때
AiScript에는 try / catch가 없습니다. API 호출이 실패하면 그 처리(핸들러)는 거기서 멈춥니다. 플러그인 자체는 멈추지 않으며, 다음 조작이나 이벤트는 동작합니다.
멈춘 것은 다음과 같이 사용자에게 보입니다.
- 노트·사용자·게시 폼의 액션에서 시작된 처리: 오류의 첫 줄이 토스트에 표시됩니다
- 위젯: 위젯의 틀 안에 오류가 표시됩니다. 다음에 화면을 다시 그리면 사라집니다
- 명령 팔레트의 명령, 이벤트, 타이머에서 시작된 처리: 실행 로그에만 남습니다
| API | 실패했을 때 |
|---|---|
Nd:http | 통신 결과(4xx / 5xx, 연결 실패)는 값으로 돌아오며 멈추지 않습니다. 지원하지 않는 메서드나 허용되지 않는 연결 대상처럼 요청을 보낼 수 없는 경우에는 멈춥니다 |
Mk:api | 멈춥니다 |
Nd:call | 멈춥니다. 확인 대화 상자가 취소된 경우에만 멈추지 않고 error 값이 돌아옵니다 |
권한이 없는 경우에는 어떤 API든 멈춥니다. 거부된 사실은 플러그인 카드의 배지에 표시되며, 토스트가 나타나기도 합니다.
Nd:call이 돌려주는 error 값의 종류는 앞으로 늘어날 수 있습니다. 취소인지 아닌지는 이름으로 판단하세요.
let res = Nd:call("notes.create", { text: "hello" })
if Core:type(res) == "error" && res.name == "user_cancelled" {
// 취소됨
}Misskey 본체의 Mk:api는 실패해도 멈추지 않고 error 값(request_failed)을 돌려줍니다. 양쪽에서 동작하는 플러그인은 Core:type(res) == "error" 분기를 남겨 두세요. NoteDeck의 Mk:api도 본체와 같은 방식으로 맞출 예정입니다.
Misskey 호환 API
Mk:api Mk:dialog Mk:confirm Mk:toast Mk:save Mk:load Mk:remove Mk:url Mk:nyaize를 쓸 수 있습니다. Misskey 본체의 플러그인을 그대로 가져올 때는 이 범위 안에서 쓰여 있는지 확인하세요. Mk:save의 값은 파일이 아니라 앱 안의 저장 영역에 들어가며, 백업에는 포함되지 않습니다.
권한
플러그인이 할 수 있는 일은 권한 설정의 "플러그인" (プラグイン) 행에서 정해집니다. 기본값은 읽기 위주의 안전한 범위와 외부 통신이며, 게시 등의 쓰기는 하나씩 허용합니다. Nd:call / Nd:http / Mk:api는 모두 이 설정으로 판정되며, 거부된 조작은 플러그인 칼럼에 거부 배지로 나타납니다. 스킬 쓰기나 AI에 대한 지시에 해당하는 조작은 설정과 관계없이 항상 거부됩니다.
메타 헤더의 permissions는 스토어에서 업데이트했을 때 "새로 요구하는 권한"을 보여 주고 다시 동의를 받기 위한 선언입니다.
이 기기의 포트에 연결하기
Nd:http는 이 기기 (localhost / 127.0.0.1 / [::1]) 로의 연결을 기본적으로 거부합니다. 직접 만든 번역 서버나 로컬 DB의 조회 엔드포인트처럼 이 기기에서 동작하는 프로세스를 호출하려면, 메타 헤더의 loopback에 포트를 선언합니다.
### {
name: "로컬 번역"
version: "1.0.0"
loopback: [8080]
}선언한 포트로의 첫 요청에서, 이 플러그인에 그 포트로의 연결을 허용할지 사용자에게 묻습니다 (사람이 노트나 사용자 메뉴, 게시 폼, 명령 팔레트에서 누른 조작 안에서만 묻습니다. 시작 시의 코드, Nd:on 이벤트, 타이머에서는 묻지 않고 거부하며, 플러그인 상세의 설정에서 허용할 수 있습니다). 허용은 본문에 묶이므로, 스토어 업데이트나 편집으로 본문이 바뀌면 다음 요청에서 다시 묻습니다. 허용한 뒤에도 요청마다의 확인은 이전과 같습니다.
AI, 다른 플러그인, 외부 앱을 대신해 동작하는 동안에는 허용이 있어도 이 기기의 포트에 연결할 수 없습니다. 위젯 / Play / 페이지는 선언을 쓸 곳이 없으므로 대상이 아닙니다. NoteDeck 자신의 포트에는 선언하거나 허용해도 연결할 수 없습니다. 응답은 한 번의 왕복으로 돌아오는 문자열이며, 다른 사람의 본문과 마찬가지로 신뢰하지 않는 것으로 다룹니다.
저장 위치
설정 폴더의 plugins/에 코드 본체 <slug>.is와 메타 정보 <slug>.meta.json5 두 파일로 저장됩니다. 목록은 메타 파일에서 만들어지므로 .is만 두면 읽히지 않습니다. 직접 둘 때는 개발자 모드의 에디터에서 새로 만든 뒤 내용을 바꿔 넣는 것이 확실합니다. 파일 이름은 표시 이름에서 만든 slug (스토어에서 왔다면 storeId)이며, 참조는 파일 안의 id로 합니다. 같은 id가 중복되면 뒤의 것은 건너뛰고 토스트로 알려 줍니다.
만들기와 배포하기
AI 칼럼에서 "○○하는 플러그인을 만들어 줘"라고 부탁하는 것이 가장 빠릅니다 (먼저 MisStore의 작성자 스킬을 설치해 둡니다). 작성과 편집의 입구는 개발자 모드에서 열립니다. 만든 것은 MisStore에서 배포할 수 있습니다 (제출 방법 / 형식).