Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions public/registry/index.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
{
"version": 1,
"updatedAt": "2026-08-11T04:14:55.998Z",
"updatedAt": "2026-08-12T04:29:41.587Z",
"plugins": {
"count": 24,
"updatedAt": "2026-08-11T04:14:55.998Z"
"count": 26,
"updatedAt": "2026-08-12T04:29:41.587Z"
},
"themes": {
"count": 54,
"updatedAt": "2026-08-11T04:14:55.998Z"
"updatedAt": "2026-08-12T04:29:41.587Z"
},
"widgets": {
"count": 45,
"updatedAt": "2026-08-11T04:14:55.998Z"
"updatedAt": "2026-08-12T04:29:41.587Z"
},
"skills": {
"count": 29,
"updatedAt": "2026-08-11T04:14:55.998Z"
"updatedAt": "2026-08-12T04:29:41.587Z"
},
"queries": {
"count": 24,
"updatedAt": "2026-08-11T04:14:55.998Z"
"updatedAt": "2026-08-12T04:29:41.587Z"
}
}
62 changes: 60 additions & 2 deletions public/registry/plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-08-11T04:14:55.998Z",
"updatedAt": "2026-08-12T04:29:41.587Z",
"plugins": [
{
"id": "plugin-smith",
Expand All @@ -21,7 +21,7 @@
"apiUrl": "https://store.notedeck.io/registry/plugins/plugin-smith/api.json",
"sha512": "e3c15224e18886a0021380b7f001d58383421958b1128384fc2d25749cb9e82bab527a7cf5042222051f6ee434111bdee8efe7b89f305d902cbe15b4592748ad",
"createdAt": "2026-05-12T11:12:35+09:00",
"updatedAt": "2026-07-20T23:42:10+09:00",
"updatedAt": "2026-08-11T13:12:16+09:00",
"authorUrl": "https://github.com/hitalin",
"license": "MIT",
"permissions": [
Expand Down Expand Up @@ -382,6 +382,35 @@
],
"iconUrl": "https://store.notedeck.io/registry/plugins/block-combo/icon.svg"
},
{
"id": "reaction-piggyback",
"name": "リアクション相乗り",
"version": "1.0.0",
"author": "hitalin",
"description": "ノートに既に付いているリアクションと同じ絵文字で相乗りします。リモートのカスタム絵文字は自鯖の同名絵文字に読み替えます",
"category": "utility",
"capabilities": [
"misskey-api",
"misskey-account"
],
"tags": [
"emoji",
"reaction",
"custom-emoji",
"remote"
],
"sourceUrl": "https://store.notedeck.io/registry/plugins/reaction-piggyback/plugin.is",
"apiUrl": "https://store.notedeck.io/registry/plugins/reaction-piggyback/api.json",
"sha512": "f237acfb9f0532b64caecfdd1817691810064d8a844e613ac36269e38577a1a64d71903261aa0952dbfcb284c27e64dff224356629a7cfdff612324c5a8a507c",
"createdAt": "2026-08-12T04:29:41.822Z",
"updatedAt": "2026-08-12T04:29:41.822Z",
"authorUrl": "https://github.com/hitalin",
"license": "MIT",
"permissions": [
"write:reactions"
],
"iconUrl": "https://store.notedeck.io/registry/plugins/reaction-piggyback/icon.svg"
},
{
"id": "kakuyomu-ruby",
"name": "ルビ振り",
Expand All @@ -405,6 +434,35 @@
"license": "MIT",
"iconUrl": "https://store.notedeck.io/registry/plugins/kakuyomu-ruby/icon.svg"
},
{
"id": "same-emoji-react",
"name": "同じ絵文字でリアクション",
"version": "1.0.0",
"author": "hitalin",
"description": "ノートに含まれるカスタム絵文字や Unicode 絵文字 (Twemoji) を拾い、同じ絵文字でリアクションします",
"category": "utility",
"capabilities": [
"misskey-api",
"misskey-account"
],
"tags": [
"emoji",
"reaction",
"custom-emoji",
"twemoji"
],
"sourceUrl": "https://store.notedeck.io/registry/plugins/same-emoji-react/plugin.is",
"apiUrl": "https://store.notedeck.io/registry/plugins/same-emoji-react/api.json",
"sha512": "5c6deb32aaf324f5a0699f99f88a6d481b96f71e0e1b77932f13f60b264faf0b4b923e657caa78328c25042c3b0e8d464428d097bb6a316bf1d301f9cdae567e",
"createdAt": "2026-08-12T13:21:58+09:00",
"updatedAt": "2026-08-12T13:21:58+09:00",
"authorUrl": "https://github.com/hitalin",
"license": "MIT",
"permissions": [
"write:reactions"
],
"iconUrl": "https://store.notedeck.io/registry/plugins/same-emoji-react/icon.svg"
},
{
"id": "snippet-palette",
"name": "定型文パレット",
Expand Down
1 change: 1 addition & 0 deletions public/registry/plugins/reaction-piggyback/api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"plugin","data":"/// @ 1.2.1\n\n// リアクション相乗り — 既に付いているリアクションと同じ絵文字で乗る\n//\n// ノートに付いているリアクションのうち、自分のサーバーでもそのまま使える\n// ものだけを候補にして順に確認する。リモートのカスタム絵文字\n// (:hoge@example.com:) は自鯖に同名の :hoge: があるときだけ、その :hoge: に\n// 読み替えて候補にする (他鯖の絵文字はそのままリアクションに使えないため)。\n// 候補は付いている数の多い順。絵文字一覧は起動中 1 回だけ取得してキャッシュする。\n\n### {\n\tname: \"リアクション相乗り\"\n\tversion: \"1.0.0\"\n\tauthor: \"@hitalin\"\n\tdescription: \"ノートに既に付いているリアクションと同じ絵文字でリアクションします。リモートのカスタム絵文字は自鯖の同名絵文字に読み替えます\"\n\tpermissions: [\"write:reactions\"]\n\tconfig: {\n\t\tuseUnicodeEmoji: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"Unicode 絵文字 (Twemoji) のリアクションも候補にする\"\n\t\t\tdefault: true\n\t\t}\n\t\tmaxCandidates: {\n\t\t\ttype: \"number\"\n\t\t\tlabel: \"確認する候補の最大数\"\n\t\t\tdefault: 8\n\t\t}\n\t}\n}\n\nlet TITLE = \"リアクション相乗り\"\n\n// Misskey Web の Mk:api は失敗時に throw せずエラー値を返す。\n// notes/reactions/create は成功時 204 (= null) なので obj 判定では弾けない\n@failed(res) {\n\tCore:type(res) == \"error\"\n}\n\n// 自鯖のカスタム絵文字名の索引を起動中キャッシュ (取得できなければ null)\nvar localNames = null\n\n@getLocalNames() {\n\tif (localNames == null) {\n\t\tlet res = Mk:api(\"emojis\", {})\n\t\tif (failed(res) || Core:type(res) != \"obj\" || !Obj:has(res, \"emojis\")) return null\n\t\tvar names = {}\n\t\teach (let e, res.emojis) {\n\t\t\tObj:set(names, e.name, true)\n\t\t}\n\t\tlocalNames = names\n\t}\n\tlocalNames\n}\n\n// リアクション文字列からカスタム絵文字名を取り出す\n// \":hoge:\" / \":hoge@example.com:\" → \"hoge\"、Unicode 絵文字なら null\n@customEmojiName(reaction) {\n\tif (reaction.len < 3) return null\n\tif (!reaction.starts_with(\":\")) return null\n\tif (!reaction.ends_with(\":\")) return null\n\tlet name = reaction.slice(1, reaction.len - 1).split(\"@\")[0]\n\tif (name == \"\") null else name\n}\n\n// 付いているリアクションを、自鯖で使える形の候補 [{ reaction, count }] に畳む。\n// available が null (絵文字一覧を取れなかった) ときはカスタム絵文字を諦める\n@buildCandidates(reactions, useUnicodeEmoji, available) {\n\tvar list = []\n\tvar seen = {} // reaction → list 内の位置\n\teach (let key, Obj:keys(reactions)) {\n\t\tlet count = Obj:get(reactions, key)\n\t\tlet name = customEmojiName(key)\n\t\tvar reaction = null\n\t\tif (name == null) {\n\t\t\t// カスタム絵文字として読めないのにコロンを含むキーは壊れているので捨てる\n\t\t\tif (useUnicodeEmoji && !key.incl(\":\")) reaction = key\n\t\t} elif (available != null && Obj:has(available, name)) {\n\t\t\t// :hoge@example.com: も自鯖に hoge があれば :hoge: として乗れる\n\t\t\treaction = `:{name}:`\n\t\t}\n\t\tif (reaction != null) {\n\t\t\tif (Obj:has(seen, reaction)) {\n\t\t\t\t// :hoge: と :hoge@example.com: が混在していたら数をまとめる\n\t\t\t\tlet at = Obj:get(seen, reaction)\n\t\t\t\tlet entry = list[at]\n\t\t\t\tentry.count = entry.count + count\n\t\t\t} else {\n\t\t\t\tObj:set(seen, reaction, list.len)\n\t\t\t\tlist.push({ reaction: reaction, count: count })\n\t\t\t}\n\t\t}\n\t}\n\tlist.sort(@(a, b) { b.count - a.count })\n}\n\nPlugin:register_note_action(TITLE, @(note) {\n\tif (Obj:has(note, \"myReaction\") && note.myReaction != null) {\n\t\tMk:dialog(TITLE, `このノートには既に {note.myReaction} でリアクションしています`, \"info\")\n\t\treturn null\n\t}\n\n\tvar reactions = {}\n\tif (Obj:has(note, \"reactions\") && note.reactions != null) reactions = note.reactions\n\tif (Obj:keys(reactions).len == 0) {\n\t\tMk:dialog(TITLE, \"このノートにはまだリアクションが付いていません\", \"info\")\n\t\treturn null\n\t}\n\n\t// カスタム絵文字のリアクションが 1 つも無ければ絵文字一覧は要らない\n\tvar needsEmojiList = false\n\teach (let key, Obj:keys(reactions)) {\n\t\tif (customEmojiName(key) != null) needsEmojiList = true\n\t}\n\tvar available = null\n\tif (needsEmojiList) {\n\t\tavailable = getLocalNames()\n\t\tif (available == null) {\n\t\t\tMk:dialog(TITLE, \"絵文字一覧を取得できなかったので、カスタム絵文字は候補から外します\", \"warning\")\n\t\t}\n\t}\n\n\tlet candidates = buildCandidates(reactions, Plugin:config.useUnicodeEmoji, available)\n\tif (candidates.len == 0) {\n\t\tMk:dialog(TITLE, \"相乗りできる絵文字がありません (自鯖に無いリモート絵文字だけです)\", \"info\")\n\t\treturn null\n\t}\n\n\tlet limit = Math:min(candidates.len, Math:max(Plugin:config.maxCandidates, 1))\n\tvar i = 0\n\tloop {\n\t\tif (i >= limit) break\n\t\tlet c = candidates[i]\n\t\tlet head = if (limit == 1) \"\" else `({i + 1}/{limit}) `\n\t\tif (Mk:confirm(TITLE, `{head}{c.reaction} ({c.count}) に相乗りしますか?`)) {\n\t\t\tlet res = Mk:api(\"notes/reactions/create\", { noteId: note.id, reaction: c.reaction })\n\t\t\tif (failed(res)) {\n\t\t\t\tMk:dialog(TITLE, \"リアクションできませんでした (アクセス許可が未付与か、既にリアクション済みの可能性があります)\", \"error\")\n\t\t\t} else {\n\t\t\t\tMk:toast(`{c.reaction} に相乗りしました`, \"success\")\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\ti = i + 1\n\t}\n})\n"}
14 changes: 14 additions & 0 deletions public/registry/plugins/reaction-piggyback/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions public/registry/plugins/reaction-piggyback/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": "reaction-piggyback",
"name": "リアクション相乗り",
"version": "1.0.0",
"author": "hitalin",
"authorUrl": "https://github.com/hitalin",
"description": "ノートに既に付いているリアクションと同じ絵文字で相乗りします。リモートのカスタム絵文字は自鯖の同名絵文字に読み替えます",
"license": "MIT",
"category": "utility",
"capabilities": ["misskey-api", "misskey-account"],
"tags": ["emoji", "reaction", "custom-emoji", "remote"],
"permissions": ["write:reactions"],
"minNoteDeckVersion": "1.8.0"
}
145 changes: 145 additions & 0 deletions public/registry/plugins/reaction-piggyback/plugin.is
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/// @ 1.2.1

// リアクション相乗り — 既に付いているリアクションと同じ絵文字で乗る
//
// ノートに付いているリアクションのうち、自分のサーバーでもそのまま使える
// ものだけを候補にして順に確認する。リモートのカスタム絵文字
// (:hoge@example.com:) は自鯖に同名の :hoge: があるときだけ、その :hoge: に
// 読み替えて候補にする (他鯖の絵文字はそのままリアクションに使えないため)。
// 候補は付いている数の多い順。絵文字一覧は起動中 1 回だけ取得してキャッシュする。

### {
name: "リアクション相乗り"
version: "1.0.0"
author: "@hitalin"
description: "ノートに既に付いているリアクションと同じ絵文字でリアクションします。リモートのカスタム絵文字は自鯖の同名絵文字に読み替えます"
permissions: ["write:reactions"]
config: {
useUnicodeEmoji: {
type: "boolean"
label: "Unicode 絵文字 (Twemoji) のリアクションも候補にする"
default: true
}
maxCandidates: {
type: "number"
label: "確認する候補の最大数"
default: 8
}
}
}

let TITLE = "リアクション相乗り"

// Misskey Web の Mk:api は失敗時に throw せずエラー値を返す。
// notes/reactions/create は成功時 204 (= null) なので obj 判定では弾けない
@failed(res) {
Core:type(res) == "error"
}

// 自鯖のカスタム絵文字名の索引を起動中キャッシュ (取得できなければ null)
var localNames = null

@getLocalNames() {
if (localNames == null) {
let res = Mk:api("emojis", {})
if (failed(res) || Core:type(res) != "obj" || !Obj:has(res, "emojis")) return null
var names = {}
each (let e, res.emojis) {
Obj:set(names, e.name, true)
}
localNames = names
}
localNames
}

// リアクション文字列からカスタム絵文字名を取り出す
// ":hoge:" / ":hoge@example.com:" → "hoge"、Unicode 絵文字なら null
@customEmojiName(reaction) {
if (reaction.len < 3) return null
if (!reaction.starts_with(":")) return null
if (!reaction.ends_with(":")) return null
let name = reaction.slice(1, reaction.len - 1).split("@")[0]
if (name == "") null else name
}

// 付いているリアクションを、自鯖で使える形の候補 [{ reaction, count }] に畳む。
// available が null (絵文字一覧を取れなかった) ときはカスタム絵文字を諦める
@buildCandidates(reactions, useUnicodeEmoji, available) {
var list = []
var seen = {} // reaction → list 内の位置
each (let key, Obj:keys(reactions)) {
let count = Obj:get(reactions, key)
let name = customEmojiName(key)
var reaction = null
if (name == null) {
// カスタム絵文字として読めないのにコロンを含むキーは壊れているので捨てる
if (useUnicodeEmoji && !key.incl(":")) reaction = key
} elif (available != null && Obj:has(available, name)) {
// :hoge@example.com: も自鯖に hoge があれば :hoge: として乗れる
reaction = `:{name}:`
}
if (reaction != null) {
if (Obj:has(seen, reaction)) {
// :hoge: と :hoge@example.com: が混在していたら数をまとめる
let at = Obj:get(seen, reaction)
let entry = list[at]
entry.count = entry.count + count
} else {
Obj:set(seen, reaction, list.len)
list.push({ reaction: reaction, count: count })
}
}
}
list.sort(@(a, b) { b.count - a.count })
}

Plugin:register_note_action(TITLE, @(note) {
if (Obj:has(note, "myReaction") && note.myReaction != null) {
Mk:dialog(TITLE, `このノートには既に {note.myReaction} でリアクションしています`, "info")
return null
}

var reactions = {}
if (Obj:has(note, "reactions") && note.reactions != null) reactions = note.reactions
if (Obj:keys(reactions).len == 0) {
Mk:dialog(TITLE, "このノートにはまだリアクションが付いていません", "info")
return null
}

// カスタム絵文字のリアクションが 1 つも無ければ絵文字一覧は要らない
var needsEmojiList = false
each (let key, Obj:keys(reactions)) {
if (customEmojiName(key) != null) needsEmojiList = true
}
var available = null
if (needsEmojiList) {
available = getLocalNames()
if (available == null) {
Mk:dialog(TITLE, "絵文字一覧を取得できなかったので、カスタム絵文字は候補から外します", "warning")
}
}

let candidates = buildCandidates(reactions, Plugin:config.useUnicodeEmoji, available)
if (candidates.len == 0) {
Mk:dialog(TITLE, "相乗りできる絵文字がありません (自鯖に無いリモート絵文字だけです)", "info")
return null
}

let limit = Math:min(candidates.len, Math:max(Plugin:config.maxCandidates, 1))
var i = 0
loop {
if (i >= limit) break
let c = candidates[i]
let head = if (limit == 1) "" else `({i + 1}/{limit}) `
if (Mk:confirm(TITLE, `{head}{c.reaction} ({c.count}) に相乗りしますか?`)) {
let res = Mk:api("notes/reactions/create", { noteId: note.id, reaction: c.reaction })
if (failed(res)) {
Mk:dialog(TITLE, "リアクションできませんでした (アクセス許可が未付与か、既にリアクション済みの可能性があります)", "error")
} else {
Mk:toast(`{c.reaction} に相乗りしました`, "success")
}
break
}
i = i + 1
}
})
Loading
Loading