Add: plugin system[WIP]

This commit is contained in:
cutls 2020-11-28 05:57:11 +09:00
parent 8873af4597
commit 7169f1147b
15 changed files with 1407 additions and 96 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ app/git
*.code-workspace
releasenote.md
app/yarn-error.log
app/js/platform/aiscript.js

10
app/aiscript.js Normal file
View File

@ -0,0 +1,10 @@
const { AiScript, parse, values, utils } = require('@syuilo/aiscript')
global.asValue = values
global.AiScript = AiScript
global.asParse = parse
global.asCommon = {
'TheDesk:console': values.FN_NATIVE((z) => {
console.log(z[0].value)
})
}
global.asUtil = utils

View File

@ -1,3 +1,9 @@
window.onload = function () {
console.log('loaded')
initPostbox()
connection()
initPlugin(plugins)
}
$.strip_tags = function (str, allowed) {
if (!str) {
return ''

152
app/js/platform/plugin.js Normal file
View File

@ -0,0 +1,152 @@
var plugins = getPlugin()
function getPlugin() {
const json = localStorage.getItem('plugins')
let ret = {
buttonOnPostbox: [],
buttonOnToot: []
}
//if(!json) return ret
//const plugins = JSON.parse(json)
const plugins = [
{
id: randomStr(20),
content: `### {
name: "マイ・ファースト・プラグイン"
version: 1
event: "buttonOnPostbox"
author: "Cutls P"
}
`
}
]
for (let plugin of plugins) {
const meta = getMeta(plugin.content)
if (!meta) continue
const type = meta.event
ret[type] ? ret[type].push(plugin) : ret[type] = [plugin]
}
return ret
}
function initPlugin() {
asCommon['TheDesk:dialog'] = asValue.FN_NATIVE((z) => {
Swal.fire({
title: z[0].value,
icon: z[2] ? z[2].value : 'info',
text: z[1] ? z[1].value : ''
})
})
asCommon['TheDesk:confirm'] = asValue.FN_NATIVE(async (z) => {
const alert = await Swal.fire({
title: z[0].value,
text: z[1].value,
icon: z[2] ? z[2].value : 'info',
showCancelButton: true
})
return asUtil.jsToVal(!!(alert.value && alert.value === true))
})
asCommon['TheDesk:css'] = asValue.FN_NATIVE((z) => {
$(escapeHTML(z[0].value)).css(escapeHTML(z[1].value), escapeHTML(z[2].value))
})
asCommon['TheDesk:api'] = asValue.FN_NATIVE(async (z) => {
try {
if (!getMeta(exe).apiGet && z[0].value == "GET") return asUtil.jsToVal(false)
if (!getMeta(exe).apiPost && (z[0].value == "POST" || z[0].value == "DELETE" || z[0].value == "PUT")) return asUtil.jsToVal(false)
const domain = localStorage.getItem(`domain_${z[3].value}`)
const at = localStorage.getItem(`acct_${z[3].value}_at`)
const start = `https://${domain}/api/${z[1].value}`
const q = {
method: z[0].value,
headers: {
'content-type': 'application/json',
Authorization:
`Bearer ${at}`
}
}
if (z[2]) q.body = z[2].value
const promise = await fetch(start, q)
const json = await promise.json()
return asUtil.jsToVal(json)
} catch (e) {
return asUtil.jsToVal(e)
}
})
const { buttonOnPostbox, init } = plugins
for (let target of buttonOnPostbox) {
const meta = getMeta(target.content)
$('#dropdown2').append(`<li><a onclick="execPlugin('${target.id}','buttonOnPostbox', null);">${escapeHTML(meta.name)}</a></li>`)
}
for (let target of init) {
const as = new AiScript(asCommon)
const meta = getMeta(target.content)
M.toast({ html: `${escapeHTML(meta.name)}を実行しました`, displayLength: 1000 })
if (target) as.exec(asParse(target.content))
}
}
function getMeta(plugin) {
try {
return AiScript.collectMetadata(asParse(plugin)).get(null)
} catch (e) {
console.error(e)
return null
}
}
async function execPlugin(id, source, args) {
const coh = plugins[source]
let exe = null
for (let plugin of coh) {
if (plugin.id == id) {
exe = plugin.content
break
}
}
const common = _.cloneDeep(asCommon)
if (source == 'buttonOnToot') {
common.DATA = args
const domain = localStorage.getItem(`domain_${args.acct_id}`)
const at = localStorage.getItem(`acct_${args.acct_id}_at`)
const start = `https://${domain}/api/v1/statuses/${args.id}`
const promise = await fetch(start, {
method: 'GET',
headers: {
'content-type': 'application/json',
Authorization:
`Bearer ${at}`
}
})
const json = await promise.json()
common.TOOT = asUtil.jsToVal(json)
common['TheDesk:changeText'] = asValue.FN_NATIVE((z) => {
if (getMeta(exe).dangerHtml) $(`[unique-id=${args.id}] .cvo`).html(z[0].value)
})
} else if (source == 'buttonOnPostbox') {
const postDt = post(null, false, true)
common.POST = asUtil.jsToVal(postDt)
common.ACCT_ID = asUtil.jsToVal(postDt.TheDeskAcctId)
common['TheDesk:postText'] = asValue.FN_NATIVE((z) => {
$('#textarea').val(z[0].value)
})
common['TheDesk:postCW'] = asValue.FN_NATIVE((z) => {
if (z[1]) $('#cw-text').val(z[1].value)
cw(z[0] ? z[0].value : false)
})
common['TheDesk:postNSFW'] = asValue.FN_NATIVE((z) => {
nsfw(z[0] ? z[0].value : false)
})
common['TheDesk:postVis'] = asValue.FN_NATIVE((z) => {
vis(z[0].value)
})
common['TheDesk:postClearbox'] = asValue.FN_NATIVE(() => {
clear()
})
common['TheDesk:postExec'] = asValue.FN_NATIVE(() => {
if (getMeta(exe).apiPost) post()
})
}
common['TheDesk:console'] = asValue.FN_NATIVE((z) => {
console.log(z[0].value)
})
const as = new AiScript(common)
if (exe) as.exec(asParse(exe))
}

View File

@ -1,14 +1,14 @@
/*保護系*/
//画像保護
function nsfw() {
if ($('#nsfw').hasClass('nsfw-avail')) {
$('#nsfw').removeClass('yellow-text')
$('#nsfw').html('visibility_off')
$('#nsfw').removeClass('nsfw-avail')
} else {
function nsfw(force) {
if (force || !$('#nsfw').hasClass('nsfw-avail')) {
$('#nsfw').addClass('yellow-text')
$('#nsfw').html('visibility')
$('#nsfw').addClass('nsfw-avail')
} else {
$('#nsfw').removeClass('yellow-text')
$('#nsfw').html('visibility_off')
$('#nsfw').removeClass('nsfw-avail')
}
}
@ -80,12 +80,7 @@ loadVis()
//コンテントワーニング
function cw(force) {
if ($('#cw').hasClass('cw-avail') || !force) {
$('#cw-text').val()
$('#cw-text').hide()
$('#cw').removeClass('yellow-text')
$('#cw').removeClass('cw-avail')
} else {
if (force || !$('#cw').hasClass('cw-avail')) {
$('#cw-text').show()
$('#cw').addClass('yellow-text')
$('#cw').addClass('cw-avail')
@ -93,6 +88,11 @@ function cw(force) {
if (cwt) {
$('#cw-text').val(cwt)
}
} else {
$('#cw-text').val()
$('#cw-text').hide()
$('#cw').removeClass('yellow-text')
$('#cw').removeClass('cw-avail')
}
}
//TLでコンテントワーニングを表示トグル

View File

@ -437,7 +437,6 @@ function draftToPost(json, acct_id, id) {
$('#post-acct-sel').val(acct_id)
$('select').formSelect()
mdCheck()
var medias = $('[toot-id=' + id + ']').attr('data-medias')
mediack = null
if(json.media_attachments) mediack = json.media_attachments[0]
//メディアがあれば
@ -684,9 +683,9 @@ function staEx(mode) {
})
return
}
function toggleAction(id) {
const elm = document.getElementById(id)
const instance = M.Dropdown.init(elm);
function toggleAction(elm) {
console.log(elm)
const instance = M.Dropdown.init(elm)
console.log(instance.isOpen)
instance.open()
}

View File

@ -334,9 +334,14 @@ function cardCheck(tlid) {
}
}
function mov(id, tlid, type) {
const dropdownTrigger = `dropdown_${tlid}_${id}`
const elm = document.getElementById(dropdownTrigger)
function mov(id, tlid, type, rand, target) {
const dropdownTrigger = `dropdown_${rand}`
let elm = document.querySelector(`#timeline_${tlid} #${dropdownTrigger}`)
if(tlid == 'notf') {
const timeline = $(target).parents('.notf-indv-box').attr('id')
elm = document.querySelector(`#${timeline} #${dropdownTrigger}`)
console.log(`#${timeline} #${dropdownTrigger}`)
}
const instance = M.Dropdown.getInstance(elm)
if(instance) {
if(instance.isOpen) return false

View File

@ -765,9 +765,10 @@ function misskeyParse(obj, mix, acct_id, tlid, popup, mutefilter) {
} else {
var actemojick = false
}
var rand = randomStr(8)
templete = templete + '<div id="pub_' + toot.id + '" class="cvo ' +
boostback + ' ' + fav_app + ' ' + rt_app + ' ' + hasmedia + '" toot-id="' + id + '" unique-id="' + uniqueid + '" data-medias="' + media_ids + ' " unixtime="' + date(obj[
key].created_at, 'unix') + '" ' + if_notf + ' onmouseover="mov(\'' + toot.id + '\',\'' + tlid + '\',\'mv\')" onclick="mov(\'' + toot.id + '\',\'' + tlid + '\',\'cl\')" onmouseout="resetmv(\'mv\')" reacted="' + reacted + '">' +
key].created_at, 'unix') + '" ' + if_notf + ' onmouseover="mov(\'' + toot.id + '\',\'' + tlid + '\',\'mv\', \''+rand+'\')" onclick="mov(\'' + toot.id + '\',\'' + tlid + '\',\'cl\', \''+rand+'\')" onmouseout="resetmv(\'mv\')" reacted="' + reacted + '">' +
'<div class="area-notice"><span class="gray sharesta">' + notice + home +
'</span></div>' +
'<div class="area-icon"><a onclick="udg(\'' + toot.user.id +

View File

@ -271,8 +271,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
var noticetext = `<span onclick="notfFilter('${toot.account.id}','${tlid}');" class=" pointer big-text ${notfFilHide}"><i class="fas fa-filter"
title="${lang.lang_parse_notffilter}">
</i><span class="voice">${lang.lang_parse_notffilter}</span></span>
<span class="cbadge cbadge-hover" title="${date(toot.created_at, 'absolute')}(${
lang.lang_parse_notftime
<span class="cbadge cbadge-hover" title="${date(toot.created_at, 'absolute')}(${lang.lang_parse_notftime
})" aria-hidden="true"><i class="far fa-clock"></i>
${date(toot.created_at, datetype)}
</span>
@ -709,8 +708,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
var featured = ` <a onclick="tagFeature('${tag.name}','${acct_id}')" class="pointer" title="add it to Featured tags">Feature</a> `
tags =
tags +
`<span class="hide" data-tag="${tag.name}" data-regTag="${tag.name.toLowerCase()}">#${
tag.name
`<span class="hide" data-tag="${tag.name}" data-regTag="${tag.name.toLowerCase()}">#${tag.name
}:
<a onclick="tl('tag','${tag.name}','${acct_id}','add')" class="pointer"
title="${lang.lang_parse_tagTL.replace(
@ -957,11 +955,9 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
bgColorCSS = bgColorCSS + bg + ','
}
bgColorCSS = `linear-gradient(90deg, ${bgColorCSS} transparent)`
var tickerdom = `<div aria-hidden="true" style="user-select:none;cursor:default;background:${bgColorCSS} !important; color:${
fontColor
var tickerdom = `<div aria-hidden="true" style="user-select:none;cursor:default;background:${bgColorCSS} !important; color:${fontColor
};width:100%; height:0.9rem; font-size:0.8rem;" class="tickers">
<img draggable="false" src="${
value.favicon
<img draggable="false" src="${value.favicon
}" style="height:100%;" onerror="this.src=\'../../img/loading.svg\'" loading="lazy">
<span style="position:relative; top:-0.2rem;">${escapeHTML(value.name)}</span>
</div>`
@ -981,8 +977,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
poll +
`<div class="quote-renote">
<div class="renote-icon">
<a onclick="udg('${toot.quote.account.id}','${acct_id}');" user="${
toot.quote.account.acct
<a onclick="udg('${toot.quote.account.id}','${acct_id}');" user="${toot.quote.account.acct
}" class="udg">
<img draggable="false" src="${toot.quote.account.avatar}" loading="lazy">
</a>
@ -994,8 +989,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
${toot.quote.content}
</div>
<div class="renote-details">
<a onclick="details('${
toot.quote.id
<a onclick="details('${toot.quote.id
}','${acct_id}','${tlid}','normal')" class="waves-effect waves-dark btn-flat details" style="padding:0">
<i class="text-darken-3 material-icons">more_vert</i>
</a>
@ -1022,6 +1016,15 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
if (trans != '') {
menuct++
}
//このトゥート内のアクションを完了させるために、適当にIDを振る
var rand = randomStr(8)
//プラグイン機構
var plugin = plugins.buttonOnToot
var pluginHtml = ''
for (let target of plugin) {
const meta = getMeta(target.content)
pluginHtml = pluginHtml + `<li><a onclick="execPlugin('${target.id}','buttonOnToot',{id: '${uniqueid}', acct_id: '${acct_id}'});">${escapeHTML(meta.name)}</a></li>`
}
templete =
templete +
`<div
@ -1032,8 +1035,8 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
'unix'
)}"
${if_notf}
onmouseover="mov('${uniqueid}','${tlid}','mv')"
onclick="mov('${uniqueid}','${tlid}','cl')"
onmouseover="mov('${uniqueid}','${tlid}','mv', '${rand}', this)"
onclick="mov('${uniqueid}','${tlid}','cl', '${rand}', this)"
onmouseout="resetmv('mv')"
>
<div class="area-notice grid"><span class="gray sharesta">${notice}${home}</span></div>
@ -1094,8 +1097,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
</a>
</div>
<div class="action ${can_rt} ${disp['rt']} ${noauth}">
<a onclick="rt('${
toot.id
<a onclick="rt('${toot.id
}','${acct_id}','${tlid}')" class="waves-effect waves-dark btn-flat actct bt-btn"
style="padding:0" title="${lang.lang_parse_bt}">
<i class="fas fa-retweet ${if_rt} rt_${toot.id}"></i>
@ -1130,8 +1132,8 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
</div>
<div class="area-side">
<div class="action ${noauth}">
<a onclick="toggleAction('trigger_${tlid}_${uniqueid}')" data-target="dropdown_${tlid}_${uniqueid}"
class="ctxMenu waves-effect waves-dark btn-flat" style="padding:0" id="trigger_${tlid}_${uniqueid}">
<a onclick="toggleAction(this)" data-target="dropdown_${rand}"
class="ctxMenu waves-effect waves-dark btn-flat" style="padding:0" id="trigger_${rand}">
<i class="text-darken-3 material-icons act-icon" aria-hidden="true">expand_more</i>
<span class="voice">Other actions</span>
</a>
@ -1144,7 +1146,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
<span class="voice">${lang.lang_parse_detail}</span>
</div>
</div>
<ul class="dropdown-content contextMenu" id="dropdown_${tlid}_${uniqueid}">
<ul class="dropdown-content contextMenu" id="dropdown_${rand}">
<li class="${viashow} via-dropdown" onclick="client('${$.strip_tags(via)}')" title="${lang.lang_parse_clientop}">
via ${escapeHTML(via)}</a>
</li>
@ -1169,6 +1171,7 @@ function parse(obj, mix, acct_id, tlid, popup, mutefilter, type) {
style="padding:0">
<i class="fas text-darken-3 fa-globe"></i>${lang.lang_parse_link}
</li>
${pluginHtml}
</ul>
</div>
`
@ -1566,7 +1569,7 @@ function mastodonBaseStreaming(acct_id) {
setTimeout(function () {
mastodonBaseWsStatus[domain] = 'available'
}, 3000)
mastodonBaseWs[domain].send(JSON.stringify({type: 'subscribe', stream: 'user'}))
mastodonBaseWs[domain].send(JSON.stringify({ type: 'subscribe', stream: 'user' }))
$('.notice_icon_acct_' + acct_id).removeClass('red-text')
}
mastodonBaseWs[domain].onmessage = function (mess) {

View File

@ -101,7 +101,6 @@ if (location.search) {
$('.mini-btn').text('expand_less')
}
}
window.onload = function () { initPostbox(); connection() }
function initPostbox() {
$('#posttgl').click(function (e) {
if (!$('#post-box').hasClass('appear')) {

View File

@ -7,8 +7,8 @@
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"construct": "cd view/make && node make --automatic && cd ../../",
"construct:store": "cd view/make && node make --automatic --store && cd ../../",
"construct": "cd view/make && node make --automatic && cd ../../ && browserify aiscript.js -o js/platform/aiscript.js",
"construct:store": "cd view/make && node make --automatic --store && cd ../../ && browserify aiscript.js -o js/platform/aiscript.js",
"dev": "npx electron ./ --dev",
"dist": "build --linux snap",
"watchview": "node view/make/make.js --automatic --watch",
@ -60,6 +60,7 @@
"license": "GPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@syuilo/aiscript": "^0.11.1",
"electron-dl": "^3.0.2",
"jimp": "^0.16.1",
"jquery": "^3.5.1",
@ -76,6 +77,7 @@
"itunes-nowplaying-mac": "0.3.1"
},
"devDependencies": {
"browserify": "^17.0.0",
"chokidar": "^3.4.3",
"electron": "^10.1.5",
"electron-builder": "^22.9.1",

View File

@ -1221,8 +1221,10 @@
<!--JS-->
<script type="text/javascript" src="../../@@node_base@@/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="../../js/platform/first.js"></script>
<script type="text/javascript" src="../../@@node_base@@/grapheme-splitter/index.js"></script>
<script type="text/javascript" src="../../js/platform/aiscript.js"></script>
<script type="text/javascript" src="../../js/platform/plugin.js"></script>
<script type="text/javascript" src="../../@@node_base@@/materialize-css/dist/js/materialize.js"></script>
<script type="text/javascript" src="../../@@node_base@@/grapheme-splitter/index.js"></script>
<script type="text/javascript" src="../../@@node_base@@/lodash/lodash.min.js"></script>
<script type="text/javascript" src="main.js"></script>
<script type="text/javascript" src="../../js/common/time.js"></script>

View File

@ -43,7 +43,7 @@
<script src="../../@@node_base@@/vue/dist/vue.min.js"></script>
<script type="text/javascript" src="setting.vue.js"></script>
<script type="text/javascript" src="../../@@node_base@@/sweetalert2/dist/sweetalert2.all.min.js"></script>
<script>function renderMem() {return false; }</script>
<script>function renderMem() { return false; }</script>
<h4>@@setting@@</h4>
<ul class="collapsible" data-collapsible="accordion">
@ -385,13 +385,6 @@
</template><br>
</template>
</div>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">keyboard</i>@@keysc@@
</div>
<div class="collapsible-body">
<h5>@@iks@@</h5>
@@okswarn@@<br>
Ctrl+Shift+1:<input type="text" style="width:11.5rem" id="oks-1">
@ -400,6 +393,15 @@
<button onclick="oks(2)" class="btn waves-effect" style="width:7.7rem;">@@set@@</button><br><br>
Ctrl+Shift+3:<input type="text" style="width:11.5rem" id="oks-3">
<button onclick="oks(3)" class="btn waves-effect" style="width:7.7rem;">@@set@@</button><br><br>
</div>
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">power</i>@@plugin@@
</div>
<div class="collapsible-body">
</div>
</li>
<li>

File diff suppressed because it is too large Load Diff

117
plugin.md Normal file
View File

@ -0,0 +1,117 @@
# TheDesk Plugin
[AiScript](https://github.com/syuilo/aiscript)で書きます。
[AiScriptの書き方](https://github.com/syuilo/aiscript/blob/master/docs/get-started.md)
## メタデータ
```
### {
name: "マイ・ファースト・プラグイン"
version: 1
event: "buttonOnPostbox"
author: "Cutls P"
}
```
これを冒頭に入れます。
* dangerHtml: true|false
`TheDesk:changeText`にアクセスするために必要です。
* apiGetl: true|false
`TheDesk:api`にGETメソッドでアクセスするときに必要です。
* apiPost: true|false
`TheDesk:api`にPOST/PUT/DELETEメソッドでアクセスするときや、`postExec`を実行するときに必要です。
### event
eventに設定できるもの
* `buttonOnPostbox`
投稿フォームの…(90°回転)で出てくるメニュー内に表示されます
* `buttonOnToot`
トゥートの詳細メニューに表示されます
* `init`
起動時(なるべく早い段階で)
追加予定…
## 変数
### buttonOnTootのとき
* DATA
```
{
id: "トゥートのID文字列",
acct_id: "アカウントのTheDesk内部ID"
}
```
* TOOT
トゥートのAPIを叩いた時と同じオブジェクトが返ります。なお、プラグインが実行されてから取得します。
プラグインの実行ボタン押下から実行までの時間差はこれによるものです。
### buttonOnPostboxのとき
* POST
投稿するときのオブジェクトがそのまま入りますが、`TheDeskAcctId`というTheDeskが内部で扱うアカウントのID(string)が入ったプロパティが追加されます。
* ACCT_ID
TheDeskが内部で扱うアカウントのID(string)
## 関数
### TheDesk:dialog(title: string, text: string, type?: string)
typeのデフォルトは'info'で、他に'error','question','success'などがある
### TheDesk:confirm(title: string, text: string, type?: string)
typeのデフォルトは'info'で、他に'error','question','success'などがある
返り値はboolean(true|false)
### TheDesk:css(query: string, attr: string, val: string)
jQueryの`$(query).css(attr, val)`に相当
### TheDesk:api(method: 'GET'|'POST'|'PUT'|'DELETE', endpoint: string, body: string, acct_id: string)
bodyにもstringを投げてください。
endpointは`v1`から書いてください。(`v1/accounts...`)
返り値はjsonのオブジェクト。
### TheDesk:changeText(body: string)
`buttonOnToot`で使用可能
該当トゥート(や他タイムラインの同一トゥート)のテキストを書き換えます。
`dangerHtml`をtrueにしてください。
HTMLを引数にすることに留意してください。
### TheDesk:postText(text: string)
`buttonOnPostbox`で使用可能
トゥートボックスの中身を書き換えます。
### TheDesk:postCW(force: boolean, text: string)
`buttonOnPostbox`で使用可能
CWを切り替えます。forceはデフォルトでfalseで、trueにするとオンに、falseにするとトグルになります。
textには警告文を入れます。
### TheDesk:postNSFW(force: boolean)
`buttonOnPostbox`で使用可能
NSFWを切り替えます。forceはデフォルトでfalseで、trueにするとオンに、falseにするとトグルになります。
### TheDesk:postVis(vis: 'public'|'unlisted'|'private'|'direct')
`buttonOnPostbox`で使用可能
公開範囲を変更します。
### TheDesk:postNSFW(force: postClearbox)
`buttonOnPostbox`で使用可能
投稿ボックスをクリアします。
### TheDesk:postExec()
`buttonOnPostbox`で使用可能
`apiPost`をtrueにしてください。
トゥートボタンを押したのと同じ挙動をします。