Compare commits

...

89 Commits

Author SHA1 Message Date
12cd2709d6 Merge branch 'develop' 2019-05-03 18:58:54 +09:00
bf54e58873 11.10.0 2019-05-03 18:58:09 +09:00
6b473e3a5c Fix #4840 2019-05-03 18:55:24 +09:00
4b68abd963 割った余りを求める関数をMisskeyPagesに追加 2019-05-03 18:48:40 +09:00
0e764a2b3e Fix external service authentication (#4846) 2019-05-03 18:38:19 +09:00
9d1ed1eb0d Some import and export fixes (#4842)
* Fix: Mastodon v2.8.0 のフォローリストがインポートできない

* Fix: エクスポートリクエストに失敗してもエラーが出ない (#4821)

* エクスポートファイルでは同一ハッシュチェックをしないように
2019-05-03 18:33:25 +09:00
a09a3465a2 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-05-03 14:49:40 +09:00
001969efaf Improve usability 2019-05-03 14:49:22 +09:00
e7c515da9a Update README.md [AUTOGEN] (#4839) 2019-05-03 14:34:41 +09:00
8367c7dd49 Update README.md [AUTOGEN] (#4837) 2019-05-03 14:32:09 +09:00
55e6cae240 Fix #4834 2019-05-03 09:16:31 +09:00
5553c3fb17 Improve usability 2019-05-03 08:27:46 +09:00
026265cb1e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-05-03 08:22:51 +09:00
289c76a802 Disable ServiceWorker 2019-05-03 08:22:44 +09:00
7c1bc1d6bc Update README.md [AUTOGEN] (#4832) 2019-05-02 19:44:59 +09:00
90cf0d32b5 Update README.md [AUTOGEN] (#4827) 2019-05-02 18:48:48 +09:00
88d934f922 Merge branch 'develop' 2019-05-02 17:57:29 +09:00
abf11bb03c 11.9.0 2019-05-02 17:57:13 +09:00
2d1f50303d Improve MisskeyPages 2019-05-02 17:55:59 +09:00
9fb7c4091f Merge branch 'develop' 2019-05-02 17:18:34 +09:00
2fdec27ab0 11.8.1 2019-05-02 17:18:11 +09:00
aff56469ed Fix bug 2019-05-02 17:15:14 +09:00
419cb7fbad Remove unwanted ! character in user token regex (#4830) 2019-05-02 06:24:32 +09:00
7882851539 Fix TypeScript semantic error (#4828)
ビルドの時こういうエラーが出てます
src/misc/aiscript/evaluator.ts(2,29): error TS7016: Could not find a declaration file for module 'seedrandom'
2019-05-01 22:56:18 +09:00
358299cf0e Merge branch 'develop' 2019-05-01 20:59:18 +09:00
5a2af24869 11.8.0-2 2019-05-01 20:58:59 +09:00
6d35872af5 Merge branch 'develop' 2019-05-01 20:57:23 +09:00
559dfdaa80 11.8.0 2019-05-01 20:56:50 +09:00
79c49bc926 ページのソースを見れるように 2019-05-01 20:48:56 +09:00
76c538ad25 🎨 2019-05-01 19:50:56 +09:00
e76e358d98 Fix bug 2019-05-01 19:50:52 +09:00
76f37671b4 🎨 2019-05-01 19:45:05 +09:00
a9fc176c3c Fix types 2019-05-01 19:31:34 +09:00
8b13e3c327 Refactor 2019-05-01 19:20:47 +09:00
c3cd6ad2d2 Refactoring 2019-05-01 18:33:11 +09:00
3444b9c9c8 Add splitStrByLine function 2019-05-01 16:33:54 +09:00
ca6fc9cd79 Remove strConcat function 2019-05-01 16:07:05 +09:00
4747ae8b61 Improve AiScript 2019-05-01 15:17:24 +09:00
b6c50d63a0 Update ja-JP.yml 2019-05-01 15:04:26 +09:00
52ebf2055e Improve AiScript 2019-05-01 14:54:34 +09:00
d0af2c2a98 Improve MisskeyPages 2019-05-01 10:37:25 +09:00
10216af48a Improve MisskeyPages 2019-05-01 10:05:33 +09:00
d0c8d537f5 Refactor 2019-05-01 09:15:29 +09:00
1903aaf351 Improve API doc
Fix #4825
2019-05-01 04:44:46 +09:00
e6cdf1b995 Fix: mention (あなた宛て) streaming にミュートが効かない (#4823) 2019-04-30 15:53:13 +09:00
2900d22cdc Update CHANGELOG.md 2019-04-30 12:38:07 +09:00
dc072d4706 Merge branch 'develop' 2019-04-30 12:21:31 +09:00
8ae14f146b 11.7.0 2019-04-30 12:20:59 +09:00
57444c6c3f Update page-editor.el.post.vue 2019-04-30 12:17:53 +09:00
759719d124 Improve MisskeyPages 2019-04-30 12:15:41 +09:00
59782973be 🎨 2019-04-30 10:08:55 +09:00
6c647ea91c Improve usability 2019-04-30 07:55:40 +09:00
c9763dabe1 変換関数を追加 2019-04-30 07:49:46 +09:00
da82754659 🎨 2019-04-30 07:43:56 +09:00
a3cc0ad18b 🎨 2019-04-30 07:41:57 +09:00
2e8e5c2751 Improve MisskeyPages
* ifブロック を追加
* ボタンやスイッチなどのテキストに変数使えるようにした
2019-04-30 06:40:02 +09:00
a60d83b101 Fix ogp 2019-04-29 18:15:12 +09:00
6d45265763 Use bigint 2019-04-29 17:38:31 +09:00
42e84b77e1 Merge branch 'develop' 2019-04-29 15:26:43 +09:00
24121cfadb 11.6.0 2019-04-29 15:26:09 +09:00
93093dd288 🎨 2019-04-29 15:20:37 +09:00
53381c04e6 🎨 2019-04-29 15:17:31 +09:00
c6b64e57f1 いくつかの文字列関数を追加 2019-04-29 15:13:04 +09:00
9158426d0a Improve usability 2019-04-29 14:51:04 +09:00
cef26853df Improve usability 2019-04-29 14:46:35 +09:00
c0673884c5 🎨 2019-04-29 14:38:06 +09:00
c98eb64598 Fix bug 2019-04-29 14:37:58 +09:00
8836bd4f3b Merge branch 'develop' 2019-04-29 11:11:17 +09:00
a58df29208 11.5.1 2019-04-29 11:10:59 +09:00
0d64a17d86 Update page.vue 2019-04-29 11:10:50 +09:00
c886c09cdb MisskeyPagesで値が0の変数が表示されない問題を修正 2019-04-29 11:08:35 +09:00
e86d0007c6 Fix error 2019-04-29 11:03:05 +09:00
624ee76e71 Fix bug 2019-04-29 11:00:41 +09:00
9406079cb7 Merge branch 'develop' 2019-04-29 09:29:48 +09:00
cd2de7f893 11.5.0 2019-04-29 09:29:21 +09:00
342061803e Update dependencies 🚀 2019-04-29 09:28:13 +09:00
05b8111c19 Pages (#4811)
* wip

* wip

* wip

* Update page-editor.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update page-editor.variable.core.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update aiscript.ts

* wip

* Update package.json

* wip

* wip

* wip

* wip

* wip

* Update page.vue

* wip

* wip

* wip

* wip

* more info

* wip fn

* wip

* wip

* wip
2019-04-29 09:11:57 +09:00
747a0b1791 Update define.ts 2019-04-28 19:56:41 +09:00
c05586b53a Improve performance 2019-04-27 11:17:03 +09:00
0cfca4a618 Fix official instance address (#4805)
いくつかのURLが misskey.xyz になってたままだったので misskey.io に訂正
2019-04-26 18:29:56 +09:00
2b8187f7ab Fix bug 2019-04-26 00:54:11 +09:00
291e7e7943 おすすめユーザーに自分自身を含まないように (#4803)
Fix #4790
2019-04-26 00:52:58 +09:00
535d10f469 Improve API console 2019-04-25 14:40:42 +09:00
4cb58c0892 Refactor 2019-04-25 13:27:07 +09:00
6721d27e3f Improve hashtag API 2019-04-25 13:25:10 +09:00
da9dd7c423 Improve API definition 2019-04-25 12:56:52 +09:00
867eb41618 Fix #4415 2019-04-25 12:37:13 +09:00
6fdff13480 Update example.yml 2019-04-25 12:24:18 +09:00
e581ead1ed Update CHANGELOG.md 2019-04-25 12:22:03 +09:00
112 changed files with 5102 additions and 284 deletions

View File

@ -132,6 +132,9 @@ drive:
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: 'aid'
# ┌─────────────────────┐

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ api-docs.json
yarn.lock
.DS_Store
/files
ormconfig.json

View File

@ -5,8 +5,46 @@ If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`)
Migration
------------------------------
#### 1
`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします:
``` json
{
"type": "postgres",
"host": "PostgreSQLのホスト",
"port": 5432,
"username": "PostgreSQLのユーザー名",
"password": "PostgreSQLのパスワード",
"database": "PostgreSQLのデータベース名",
"entities": ["src/models/entities/*.ts"],
"migrations": ["migration/*.ts"],
"cli": {
"migrationsDir": "migration"
}
}
```
上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。
#### 2
```
npm i -g ts-node
```
#### 3
```
ts-node ./node_modules/typeorm/cli.js migration:run
```
How to migrate to v11 from v10
------------------------------
### 移行の注意点
**以下のデータは引き継がれません**
* 通知
* リモートの投稿
* リバーシの対局
### 手順
1. v11をインストールしたい場所に syuilo/misskey をクローン
2. config を設定する
* PostgreSQL(`db`)の設定とは別に、v10からMongoDBの設定をコピペしてくる(例は下にあります)
@ -35,6 +73,111 @@ mongodb:
8. master ブランチに戻す
9. enjoy
11.10.0 (2019/05/03)
-------------------
### 注意
このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください
### Improvements
* MisskeyPagesに割った余りを求める関数を追加
* Mastodon v2.8.0 のフォローリストをインポートできるように
* エクスポートリクエストに失敗したらエラーを表示するように
* エクスポートファイルでは同一ハッシュチェックをしないように
### Fixes
* 2段階認証を設定するとログインできなくなる問題を修正
* ファイルをアップロードできないことがある問題を修正
* リモートファイルをキャッシュしない設定だとサムネイル時にオリジナル画像が表示されない問題を修正
* 外部サービス連携の不具合を修正
11.9.0 (2019/05/02)
-------------------
### Improvements
* MisskeyPagesで編集時にページブロックをドラッグで並べ替えられるように
* MisskeyPagesにカウンターボタンブロックを追加
11.8.1 (2019/05/02)
-------------------
### Fixes
* リモートファイルをキャッシュしないオプション有効時にファイルが作成できない問題を修正
11.8.0-2 (2019/05/01)
-------------------
* 11.8.0 のリリース内容が 11.7.0 と同一だったのを修正
11.8.0 (2019/05/01)
-------------------
### Improvements
* MisskeyPagesで関数を作成できるように
* MisskeyPagesでソースを表示できるように
* MisskeyPagesにシードを与えるランダム関数を追加
* MisskeyPagesに複数行テキストをテキストのリストに変換する関数を追加
### Fixes
* APIドキュメントが見れなくなっていたのを修正
* mention (あなた宛て) streaming にミュートが効かない問題を修正
* デザインの調整
11.7.0 (2019/04/30)
-------------------
### Improvements
* MisskeyPagesに ifブロック を追加
* MisskeyPagesに テキストエリア を追加
* MisskeyPagesに 複数行テキスト入力 を追加
* MisskeyPagesに 投稿フォーム を追加
* MisskeyPagesに 変換系関数 を追加
* MisskeyPagesに 環境変数 URL を追加
* MisskeyPagesでボタンやスイッチなどのテキストに変数使えるように
### Fixes
* OGPのサイト名を修正
* デザインの調整
11.6.0 (2019/04/29)
-------------------
### Improvements
* AiScriptにいくつかの文字列操作関数を追加
* ページ編集画面にページへのリンクを表示するように
### Fixes
* MisskeyPagesで数値入力が文字列として扱われる問題を修正
* デザインの調整
11.5.1 (2019/04/29)
-------------------
### Fixes
* MisskeyPagesで環境変数を別の変数内で使えない問題を修正
* MisskeyPagesで値が0の変数が表示されない問題を修正
11.5.0 (2019/04/29)
-------------------
### 注意
このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](migration)の手順を実行してください
### New features
#### MisskeyPages
ページ(記事)を作成できるように。
* 後から何度でも編集できる
* アイキャッチを設定できる
* フォントを設定できる
* 画像を好きな位置に挿入できる
* URLを決められる
* タイトルを設定できる
* 見出しを設定できる
* ページの要約を設定できる(URLプレビュー時などに便利)
* 変数や式(aka AiScript)を使用して動的なページも作れる
* 目次自動生成(coming soon)
ページを気に入ったら「いいね」しよう (coming soon)
### Improvements
* APIコンソールでパラメータテンプレートを表示するように
### Fixes
* おすすめユーザーに自分自身が含まれる問題を修正
* ユーザーサジェストで表示名が変わらない問題を修正
11.4.0 (2019/04/25)
-------------------
### Improvements

View File

@ -1,6 +1,6 @@
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
[![Misskey](/assets/title.png)](https://misskey.xyz/)
[![Misskey](/assets/title.png)](https://misskey.io/)
================================================================
[![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey)
@ -10,7 +10,7 @@
**A forever evolving, sophisticated microblogging platform.**
<p align="justify">
<a href="https://misskey.xyz">Misskey</a> is a decentralized microblogging platform born on Earth.
<a href="https://misskey.io">Misskey</a> is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms.
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://joinmisskey.github.io/">Find an instance!</a>
@ -118,10 +118,10 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1.png?token-time=2145916800&token-hash=FMV7cPKBD1TU2WTbl1jg6AcdKSvTb2BSFcDhgc-EO8w%3D" alt="gutfuckllc" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/12718187" alt="Peter G." width="100"></td>
<td><img src="https://c8.patreon.com/2/200/18833336" alt="itiradi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1.jpe?token-time=2145916800&token-hash=UQRWf01TwHDV4Cls1K0YAOAjM29ssif7hLVq0ESQ0hs%3D" alt="nemu" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
@ -129,10 +129,10 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
<td><a href="https://www.patreon.com/user?u=18833336">itiradi</a></td>
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
@ -146,28 +146,30 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/83884b38afc74d4cbe83c30a13b10edd/1.png?token-time=2145916800&token-hash=R5Tog8RWg0rguRoCIoir3lThokrdPvs8Utfikhc0nhY%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1.jpe?token-time=2145916800&token-hash=EWxXhVbZYH7KB4IDT3joc8TbIg8zPO40x1r5IDn3R7c%3D" alt="Hiratake" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1.jpeg?token-time=2145916800&token-hash=d8jBQLMOHD87KtXs5C9fk1o58DMF73pQ-dYH3uZJPBE%3D" alt="Gargron" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
<td><a href="https://www.patreon.com/damillora">Damillora</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/Corset">CG</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Wed, 24 Apr 2019 05:56:07 UTC
**Last updated:** Fri, 03 May 2019 05:33:07 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

View File

@ -65,6 +65,7 @@ common:
trash: "ゴミ箱"
drive: "ドライブ"
pages: "ページ"
messaging: "トーク"
home: "ホーム"
deck: "デッキ"
@ -1813,26 +1814,6 @@ docs:
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
edit-this-page-on-github-link: "このページをGitHubで編集"
api:
entities:
properties: "プロパティ"
endpoints:
params: "パラメータ"
no-params: "パラメータはありません"
res: "レスポンス"
require-credential: "このエンドポイントは認証情報が必須です。"
require-permission: "このエンドポイントは{permission}の権限を必要とします。"
has-limit: "レートリミットがあります。"
duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。"
min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。"
show-src: "このエンドポイントのソースコードも閲覧できます。"
show-src-link: "コードをGitHubで見る"
generated: "このドキュメントはAPI定義に基づき自動生成されています。"
props:
name: "名前"
type: "型"
description: "説明"
dev/views/index.vue:
manage-apps: "アプリの管理"
@ -1857,3 +1838,256 @@ dev/views/new-app.vue:
authority: "権限"
authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
pages:
new-page: "ページの作成"
edit-page: "ページの編集"
read-page: "ソースを表示中"
page-created: "ページを作成しました"
page-updated: "ページを更新しました"
are-you-sure-delete: "このページを削除しますか?"
page-deleted: "ページを削除しました"
edit-this-page: "このページを編集"
view-source: "ソースを表示"
view-page: "ページを見る"
inspector: "インスペクター"
content: "ページブロック"
variables: "変数"
variables-info: "変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。"
variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。"
variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。"
variables-info4: "関数を使うと、値の算出処理を再利用可能な形にまとめることができます。関数を作るには、「関数」タイプの変数を作成します。関数にはスロット(引数)を設定することができ、スロットの値は関数内で変数として利用可能です。また、AiScript標準で関数を引数に取る関数(高階関数と呼ばれます)も存在します。関数は予め定義しておくほかに、このような高階関数のスロットに即席でセットすることもできます。"
more-details: "詳しい説明"
title: "タイトル"
url: "ページURL"
summary: "ページの要約"
align-center: "中央寄せ"
font: "フォント"
fontSerif: "セリフ"
fontSansSerif: "サンセリフ"
set-eye-catchig-image: "アイキャッチ画像を設定"
remove-eye-catchig-image: "アイキャッチ画像を削除"
choose-block: "ブロックを追加"
select-type: "種類を選択"
enter-variable-name: "変数名を決めてください"
the-variable-name-is-already-used: "その変数名は既に使われています"
content-blocks: "コンテンツ"
input-blocks: "入力"
special-blocks: "特殊"
post-from-post-form: "この内容を投稿"
posted-from-post-form: "投稿しました"
blocks:
text: "テキスト"
textarea: "テキストエリア"
section: "セクション"
image: "画像"
button: "ボタン"
if: "もし"
_if:
variable: "変数"
post: "投稿フォーム"
_post:
text: "内容"
textInput: "テキスト入力"
_textInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
textareaInput: "複数行テキスト入力"
_textareaInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
numberInput: "数値入力"
_numberInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
switch: "スイッチ"
_switch:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
counter: "カウンター"
_counter:
name: "変数名"
text: "タイトル"
inc: "増加値"
_button:
text: "タイトル"
action: "ボタンを押したときの動作"
_action:
dialog: "ダイアログを表示する"
_dialog:
content: "内容"
resetRandom: "乱数をリセット"
script:
categories:
flow: "制御"
logical: "論理演算"
operation: "計算"
comparison: "比較"
random: "ランダム"
value: "値"
fn: "関数"
text: "テキスト操作"
convert: "変換"
blocks:
text: "テキスト"
multiLineText: "テキスト(複数行)"
textList: "テキストのリスト"
_textList:
info: "ひとつひとつを改行で区切ってください"
strLen: "テキストの長さ"
_strLen:
arg1: "テキスト"
strPick: "文字取り出し"
_strPick:
arg1: "テキスト"
arg2: "文字の位置"
strReplace: "テキスト置き換え"
_strReplace:
arg1: "テキスト"
arg2: "置き換え前"
arg3: "置き換え後"
strReverse: "テキストを反転"
_strReverse:
arg1: "テキスト"
join: "テキストを連結"
_join:
arg1: "リスト"
arg2: "区切り"
add: "+ 足す"
_add:
arg1: "A"
arg2: "B"
subtract: "- 引く"
_subtract:
arg1: "A"
arg2: "B"
multiply: "× 掛ける"
_multiply:
arg1: "A"
arg2: "B"
divide: "÷ 割る"
_divide:
arg1: "A"
arg2: "B"
remind: "÷ 割った余り"
_remind:
arg1: "A"
arg2: "B"
eq: "AとBが同じ"
_eq:
arg1: "A"
arg2: "B"
notEq: "AとBが異なる"
_notEq:
arg1: "A"
arg2: "B"
and: "AかつB"
_and:
arg1: "A"
arg2: "B"
or: "AまたはB"
_or:
arg1: "A"
arg2: "B"
lt: "< AがBより小さい"
_lt:
arg1: "A"
arg2: "B"
gt: "> AがBより大きい"
_gt:
arg1: "A"
arg2: "B"
ltEq: "<= AがBと同じか小さい"
_ltEq:
arg1: "A"
arg2: "B"
gtEq: ">= AがBと同じか大きい"
_gtEq:
arg1: "A"
arg2: "B"
if: "分岐"
_if:
arg1: "もし"
arg2: "なら"
arg3: "そうでなければ"
not: "否定"
_not:
arg1: "否定"
random: "ランダム"
_random:
arg1: "確率"
rannum: "乱数"
_rannum:
arg1: "最小"
arg2: "最大"
randomPick: "リストからランダムに選択"
_randomPick:
arg1: "リスト"
dailyRandom: "ランダム (ユーザーごとに日替わり)"
_dailyRandom:
arg1: "確率"
dailyRannum: "乱数 (ユーザーごとに日替わり)"
_dailyRannum:
arg1: "最小"
arg2: "最大"
dailyRandomPick: "リストからランダムに選択 (ユーザーごとに日替わり)"
_dailyRandomPick:
arg1: "リスト"
seedRandom: "ランダム (シード)"
_seedRandom:
arg1: "シード"
arg2: "確率"
seedRannum: "乱数 (シード)"
_seedRannum:
arg1: "シード"
arg2: "最小"
arg3: "最大"
seedRandomPick: "リストからランダムに選択 (シード)"
_seedRandomPick:
arg1: "シード"
arg2: "リスト"
number: "数値"
stringToNumber: "テキストを数値に"
_stringToNumber:
arg1: "テキスト"
numberToString: "数値をテキストに"
_numberToString:
arg1: "数値"
splitStrByLine: "テキストを行で分割"
_splitStrByLine:
arg1: "テキスト"
ref: "変数"
fn: "関数"
_fn:
slots: "スロット"
slots-info: "スロットひとつひとつを改行で区切ってください"
arg1: "出力"
for: "繰り返し"
_for:
arg1: "回数"
arg2: "処理"
typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
thereIsEmptySlot: "スロット{slot}が空です!"
types:
string: "テキスト"
number: "数値"
boolean: "フラグ"
array: "リスト"
stringArray: "テキストのリスト"
emptySlot: "空のスロット"
enviromentVariables: "環境変数"
pageVariables: "ページ要素"
argVariables: "入力スロット"

View File

@ -0,0 +1,31 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class Pages1556348509290 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`);
await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `);
await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `);
await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `);
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`);
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`);
await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`);
await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`);
await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`);
await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`);
await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`);
await queryRunner.query(`DROP TABLE "page"`);
await queryRunner.query(`DROP TYPE "page_visibility_enum"`);
}
}

View File

@ -0,0 +1,23 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class UserProfile1556746559567 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`UPDATE "user_profile" SET github = FALSE`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubId"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "githubId" VARCHAR(64)`);
await queryRunner.query(`UPDATE "user_profile" SET discord = FALSE`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordExpiresDate"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "discordExpiresDate" VARCHAR(64)`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`UPDATE "user_profile" SET github = FALSE`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubId"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "githubId" INTEGER`);
await queryRunner.query(`UPDATE "user_profile" SET discord = FALSE`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordExpiresDate"`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "discordExpiresDate" INTEGER`);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "11.4.0",
"version": "11.10.0",
"codename": "daybreak",
"repository": {
"type": "git",
@ -80,6 +80,7 @@
"@types/request-promise-native": "1.0.15",
"@types/request-stats": "3.0.0",
"@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.28",
"@types/sharp": "0.22.1",
"@types/showdown": "1.9.2",
"@types/speakeasy": "2.0.4",
@ -93,13 +94,13 @@
"@types/websocket": "0.0.40",
"@types/ws": "6.0.1",
"animejs": "3.0.1",
"apexcharts": "3.6.8",
"apexcharts": "3.6.9",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"bootstrap-vue": "2.0.0-rc.13",
"bull": "3.7.0",
"bull": "3.8.1",
"cafy": "15.1.1",
"chai": "4.2.0",
"chalk": "2.4.2",
@ -120,7 +121,7 @@
"feed": "2.0.4",
"file-type": "10.11.0",
"fuckadblock": "3.2.1",
"gulp": "4.0.0",
"gulp": "4.0.1",
"gulp-cssnano": "2.1.3",
"gulp-imagemin": "5.0.3",
"gulp-mocha": "6.0.0",
@ -139,7 +140,7 @@
"is-root": "2.1.0",
"is-svg": "4.1.0",
"js-yaml": "3.13.1",
"jsdom": "14.1.0",
"jsdom": "15.0.0",
"json5": "2.1.0",
"json5-loader": "2.0.0",
"katex": "0.10.1",
@ -159,7 +160,7 @@
"loader-utils": "1.2.3",
"lolex": "3.1.0",
"lookup-dns-cache": "2.1.0",
"minio": "7.0.6",
"minio": "7.0.7",
"mocha": "6.1.3",
"moji": "0.5.1",
"moment": "2.24.0",
@ -199,7 +200,8 @@
"rimraf": "2.6.3",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sharp": "0.22.0",
"seedrandom": "3.0.1",
"sharp": "0.22.1",
"showdown": "1.9.0",
"showdown-highlightjs-extension": "0.1.2",
"speakeasy": "2.0.0",
@ -208,7 +210,7 @@
"stylus": "0.54.5",
"stylus-loader": "3.0.2",
"summaly": "2.2.0",
"systeminformation": "4.1.5",
"systeminformation": "4.1.6",
"syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "1.2.3",
"textarea-caret": "3.1.0",
@ -232,7 +234,7 @@
"vue-color": "2.7.0",
"vue-content-loading": "1.6.0",
"vue-cropperjs": "3.0.0",
"vue-i18n": "8.10.0",
"vue-i18n": "8.11.1",
"vue-js-modal": "1.3.28",
"vue-json-pretty": "1.6.0",
"vue-loader": "15.7.0",

View File

@ -0,0 +1,42 @@
export function collectPageVars(content) {
const pageVars = [];
const collect = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'textInput') {
pageVars.push({
name: x.name,
type: 'string',
value: x.default || ''
});
} else if (x.type === 'textareaInput') {
pageVars.push({
name: x.name,
type: 'string',
value: x.default || ''
});
} else if (x.type === 'numberInput') {
pageVars.push({
name: x.name,
type: 'number',
value: x.default || 0
});
} else if (x.type === 'switch') {
pageVars.push({
name: x.name,
type: 'boolean',
value: x.default || false
});
} else if (x.type === 'counter') {
pageVars.push({
name: x.name,
type: 'number',
value: 0
});
} else if (x.children) {
collect(x.children);
}
}
};
collect(content);
return pageVars;
}

View File

@ -1,7 +1,7 @@
<template>
<div class="felqjxyj" :class="{ splash }">
<div class="bg" ref="bg" @click="onBgClick"></div>
<div class="main" ref="main">
<div class="main" ref="main" :class="{ round: $store.state.device.roundedCorners }">
<template v-if="type == 'signin'">
<mk-signin/>
</template>
@ -22,7 +22,14 @@
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>
<ui-select v-if="select" v-model="selectedValue" autofocus>
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</template>
<template v-else>
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
</optgroup>
</template>
</ui-select>
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
@ -222,15 +229,17 @@ export default Vue.extend({
width calc(100% - 32px)
text-align center
background var(--face)
border-radius 8px
color var(--faceText)
opacity 0
&.round
border-radius 8px
> .icon
font-size 32px
&.success
color #37ec92
color #85da5a
&.error
color #ec4137

View File

@ -36,7 +36,7 @@ export default Vue.extend({
return {
hide: true
};
}
},
computed: {
style(): any {
let url = `url(${

View File

@ -13,8 +13,8 @@
@click="navigate(user)"
tabindex="-1"
>
<mk-avatar class="avatar" :user="user"/>
<span class="name"><mk-user-name :user="user"/></span>
<mk-avatar class="avatar" :user="user" :key="user.id"/>
<span class="name"><mk-user-name :user="user" :key="user.id"/></span>
<span class="username">@{{ user | acct }}</span>
</li>
</ol>

View File

@ -0,0 +1,54 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template>
<section class="xfhsjczc">
<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
<ui-select v-model="value.action">
<template #label>{{ $t('blocks._button.action') }}</template>
<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
</ui-select>
<ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt
};
},
created() {
if (this.value.text == null) Vue.set(this.value, 'text', '');
if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
if (this.value.content == null) Vue.set(this.value, 'content', null);
},
});
</script>
<style lang="stylus" scoped>
.xfhsjczc
padding 0 16px 0 16px
</style>

View File

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.counter') }}</template>
<section style="padding: 0 16px 0 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._counter.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._counter.text') }}</span></ui-input>
<ui-input v-model="value.inc" type="number"><span>{{ $t('blocks._counter.increment') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt, faMagic
};
},
created() {
if (this.value.name == null) Vue.set(this.value, 'name', '');
},
});
</script>

View File

@ -0,0 +1,90 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faQuestion"/> {{ $t('blocks.if') }}</template>
<template #func>
<button @click="add()">
<fa :icon="faPlus"/>
</button>
</template>
<section class="romcojzs">
<ui-select v-model="value.var">
<template #label>{{ $t('blocks._if.variable') }}</template>
<option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
<optgroup :label="$t('script.pageVariables')">
<option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option>
</optgroup>
<optgroup :label="$t('script.enviromentVariables')">
<option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
</optgroup>
</ui-select>
<x-blocks class="children" v-model="value.children" :ai-script="aiScript"/>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
inject: ['getPageBlockList'],
props: {
value: {
required: true
},
aiScript: {
required: true,
},
},
data() {
return {
faPlus, faQuestion
};
},
beforeCreate() {
this.$options.components.XBlocks = require('../page-editor.blocks.vue').default
},
created() {
if (this.value.children == null) Vue.set(this.value, 'children', []);
if (this.value.var === undefined) Vue.set(this.value, 'var', null);
},
methods: {
async add() {
const { canceled, result: type } = await this.$root.dialog({
type: null,
title: this.$t('choose-block'),
select: {
groupedItems: this.getPageBlockList()
},
showCancelButton: true
});
if (canceled) return;
const id = uuid.v4();
this.value.children.push({ id, type });
},
}
});
</script>
<style lang="stylus" scoped>
.romcojzs
padding 0 16px 16px 16px
</style>

View File

@ -0,0 +1,78 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template>
<template #func>
<button @click="choose()">
<fa :icon="faFolderOpen"/>
</button>
</template>
<section class="oyyftmcf">
<x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
import XFileThumbnail from '../../drive-file-thumbnail.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer, XFileThumbnail
},
props: {
value: {
required: true
},
},
data() {
return {
file: null,
faPencilAlt, faImage, faFolderOpen
};
},
created() {
if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null);
},
mounted() {
if (this.value.fileId == null) {
this.choose();
} else {
this.$root.api('drive/files/show', {
fileId: this.value.fileId
}).then(file => {
this.file = file;
});
}
},
methods: {
async choose() {
this.$chooseDriveFile({
multiple: false
}).then(file => {
this.file = file;
this.value.fileId = file.id;
});
},
}
});
</script>
<style lang="stylus" scoped>
.oyyftmcf
> .preview
height 150px
</style>

View File

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.numberInput') }}</template>
<section style="padding: 0 16px 0 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._numberInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._numberInput.text') }}</span></ui-input>
<ui-input v-model="value.default" type="number"><span>{{ $t('blocks._numberInput.default') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt, faMagic
};
},
created() {
if (this.value.name == null) Vue.set(this.value, 'name', '');
},
});
</script>

View File

@ -0,0 +1,40 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faPaperPlane"/> {{ $t('blocks.post') }}</template>
<section style="padding: 0 16px 16px 16px;">
<ui-textarea v-model="value.text">{{ $t('blocks._post.text') }}</ui-textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faPaperPlane
};
},
created() {
if (this.value.text == null) Vue.set(this.value, 'text', '');
},
});
</script>

View File

@ -0,0 +1,103 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template>
<template #func>
<button @click="rename()">
<fa :icon="faPencilAlt"/>
</button>
<button @click="add()">
<fa :icon="faPlus"/>
</button>
</template>
<section class="ilrvjyvi">
<x-blocks class="children" v-model="value.children" :ai-script="aiScript"/>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
inject: ['getPageBlockList'],
props: {
value: {
required: true
},
aiScript: {
required: true,
},
},
data() {
return {
faStickyNote, faPlus, faPencilAlt
};
},
beforeCreate() {
this.$options.components.XBlocks = require('../page-editor.blocks.vue').default
},
created() {
if (this.value.title == null) Vue.set(this.value, 'title', null);
if (this.value.children == null) Vue.set(this.value, 'children', []);
},
mounted() {
if (this.value.title == null) {
this.rename();
}
},
methods: {
async rename() {
const { canceled, result: title } = await this.$root.dialog({
title: 'Enter title',
input: {
type: 'text',
default: this.value.title
},
showCancelButton: true
});
if (canceled) return;
this.value.title = title;
},
async add() {
const { canceled, result: type } = await this.$root.dialog({
type: null,
title: this.$t('choose-block'),
select: {
groupedItems: this.getPageBlockList()
},
showCancelButton: true
});
if (canceled) return;
const id = uuid.v4();
this.value.children.push({ id, type });
},
}
});
</script>
<style lang="stylus" scoped>
.ilrvjyvi
> .children
padding 16px
</style>

View File

@ -0,0 +1,48 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template>
<section class="kjuadyyj">
<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input>
<ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt, faMagic
};
},
created() {
if (this.value.name == null) Vue.set(this.value, 'name', '');
},
});
</script>
<style lang="stylus" scoped>
.kjuadyyj
padding 0 16px 16px 16px
</style>

View File

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.textInput') }}</template>
<section style="padding: 0 16px 0 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._textInput.text') }}</span></ui-input>
<ui-input v-model="value.default" type="text"><span>{{ $t('blocks._textInput.default') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt, faMagic
};
},
created() {
if (this.value.name == null) Vue.set(this.value, 'name', '');
},
});
</script>

View File

@ -0,0 +1,58 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template>
<section class="ihymsbbe">
<textarea v-model="value.text"></textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faAlignLeft,
};
},
created() {
if (this.value.text == null) Vue.set(this.value, 'text', '');
},
});
</script>
<style lang="stylus" scoped>
.ihymsbbe
> textarea
display block
-webkit-appearance none
-moz-appearance none
appearance none
width 100%
min-width 100%
min-height 150px
border none
box-shadow none
padding 16px
background transparent
color var(--text)
font-size 14px
</style>

View File

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.textareaInput') }}</template>
<section style="padding: 0 16px 16px 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._textareaInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._textareaInput.text') }}</span></ui-input>
<ui-textarea v-model="value.default"><span>{{ $t('blocks._textareaInput.default') }}</span></ui-textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faBolt, faMagic
};
},
created() {
if (this.value.name == null) Vue.set(this.value, 'name', '');
},
});
</script>

View File

@ -0,0 +1,58 @@
<template>
<x-container @remove="() => $emit('remove')" :draggable="true">
<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.textarea') }}</template>
<section class="ihymsbbe">
<textarea v-model="value.text"></textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faAlignLeft,
};
},
created() {
if (this.value.text == null) Vue.set(this.value, 'text', '');
},
});
</script>
<style lang="stylus" scoped>
.ihymsbbe
> textarea
display block
-webkit-appearance none
-moz-appearance none
appearance none
width 100%
min-width 100%
min-height 150px
border none
box-shadow none
padding 16px
background transparent
color var(--text)
font-size 14px
</style>

View File

@ -0,0 +1,65 @@
<template>
<x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5">
<component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="removeItem" :key="block.id" :ai-script="aiScript"/>
</x-draggable>
</template>
<script lang="ts">
import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import XSection from './els/page-editor.el.section.vue';
import XText from './els/page-editor.el.text.vue';
import XTextarea from './els/page-editor.el.textarea.vue';
import XImage from './els/page-editor.el.image.vue';
import XButton from './els/page-editor.el.button.vue';
import XTextInput from './els/page-editor.el.text-input.vue';
import XTextareaInput from './els/page-editor.el.textarea-input.vue';
import XNumberInput from './els/page-editor.el.text-input.vue';
import XSwitch from './els/page-editor.el.switch.vue';
import XIf from './els/page-editor.el.if.vue';
import XPost from './els/page-editor.el.post.vue';
import XCounter from './els/page-editor.el.counter.vue';
export default Vue.extend({
components: {
XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter
},
props: {
value: {
type: Array,
required: true
},
aiScript: {
required: true,
},
},
computed: {
blocks() {
return this.value;
}
},
methods: {
updateItem(v) {
const i = this.blocks.findIndex(x => x.id === v.id);
const newValue = [
...this.blocks.slice(0, i),
v,
...this.blocks.slice(i + 1)
];
this.$emit('input', newValue);
},
removeItem(el) {
const i = this.blocks.findIndex(x => x.id === el.id);
const newValue = [
...this.blocks.slice(0, i),
...this.blocks.slice(i + 1)
];
this.$emit('input', newValue);
},
}
});
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
<header>
<div class="title"><slot name="header"></slot></div>
<div class="buttons">
<slot name="func"></slot>
<button v-if="removable" @click="remove()">
<fa :icon="faTrashAlt"/>
</button>
<button v-if="draggable" class="drag-handle">
<fa :icon="faBars"/>
</button>
<button @click="toggleContent(!showBody)">
<template v-if="showBody"><fa icon="angle-up"/></template>
<template v-else><fa icon="angle-down"/></template>
</button>
</div>
</header>
<p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
<div v-show="showBody">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBars } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../i18n';
export default Vue.extend({
i18n: i18n('pages'),
props: {
expanded: {
type: Boolean,
default: true
},
removable: {
type: Boolean,
default: true
},
draggable: {
type: Boolean,
default: false
},
error: {
required: false,
default: null
},
warn: {
required: false,
default: null
}
},
data() {
return {
showBody: this.expanded,
faTrashAlt, faBars
};
},
methods: {
toggleContent(show: boolean) {
this.showBody = show;
this.$emit('toggle', show);
},
remove() {
this.$emit('remove');
}
}
});
</script>
<style lang="stylus" scoped>
.cpjygsrt
overflow hidden
background var(--face)
border solid 2px var(--pageBlockBorder)
border-radius 6px
&:hover
border solid 2px var(--pageBlockBorderHover)
&.warn
border solid 2px #dec44c
&.error
border solid 2px #f00
& + .cpjygsrt
margin-top 16px
> header
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color var(--faceHeaderText)
box-shadow 0 1px rgba(#000, 0.07)
> [data-icon]
margin-right 6px
&:empty
display none
> .buttons
position absolute
z-index 2
top 0
right 0
> button
padding 0
width 42px
font-size 0.9em
line-height 42px
color var(--faceTextButton)
&:hover
color var(--faceTextButtonHover)
&:active
color var(--faceTextButtonActive)
.drag-handle
cursor move
> .warn
color #b19e49
margin 0
padding 16px 16px 0 16px
font-size 14px
> .error
color #f00
margin 0
padding 16px 16px 0 16px
font-size 14px
</style>

View File

@ -0,0 +1,267 @@
<template>
<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn">
<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
<template #func>
<button @click="changeType()">
<fa :icon="faPencilAlt"/>
</button>
</template>
<section v-if="value.type === null" class="pbglfege" @click="changeType()">
{{ $t('script.emptySlot') }}
</section>
<section v-else-if="value.type === 'text'" class="tbwccoaw">
<input v-model="value.value"/>
</section>
<section v-else-if="value.type === 'multiLineText'" class="tbwccoaw">
<textarea v-model="value.value"></textarea>
</section>
<section v-else-if="value.type === 'textList'" class="tbwccoaw">
<textarea v-model="value.value" :placeholder="$t('script.blocks._textList.info')"></textarea>
</section>
<section v-else-if="value.type === 'number'" class="tbwccoaw">
<input v-model="value.value" type="number"/>
</section>
<section v-else-if="value.type === 'ref'" class="hpdwcrvs">
<select v-model="value.value">
<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
<optgroup :label="$t('script.argVariables')">
<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
</optgroup>
<optgroup :label="$t('script.pageVariables')">
<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
</optgroup>
<optgroup :label="$t('script.enviromentVariables')">
<option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
</optgroup>
</select>
</section>
<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
<ui-textarea v-model="slots">
<span>{{ $t('script.blocks._fn.slots') }}</span>
<template #desc>{{ $t('script.blocks._fn.slots-info') }}</template>
</ui-textarea>
<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
</section>
<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
</section>
<section v-else class="" style="padding:16px;">
<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import XContainer from './page-editor.container.vue';
import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import { isLiteralBlock, funcDefs, blockDefs } from '../../../../../../misc/aiscript/index';
import * as uuid from 'uuid';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
inject: ['getScriptBlockList'],
props: {
getExpectedType: {
required: false,
default: null
},
value: {
required: true
},
title: {
required: false
},
removable: {
required: false,
default: false
},
aiScript: {
required: true,
},
name: {
required: true,
},
fnSlots: {
required: false,
},
},
data() {
return {
error: null,
warn: null,
slots: '',
faPencilAlt
};
},
computed: {
icon(): any {
if (this.value.type === null) return null;
if (this.value.type.startsWith('fn:')) return faPlug;
return blockDefs.find(x => x.type === this.value.type).icon;
},
typeText(): any {
if (this.value.type === null) return null;
if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1];
return this.$t(`script.blocks.${this.value.type}`);
},
},
watch: {
slots() {
this.value.value.slots = this.slots.split('\n').map(x => ({
name: x,
type: null
}));
}
},
beforeCreate() {
this.$options.components.XV = require('./page-editor.script-block.vue').default;
},
created() {
if (this.value.value == null) Vue.set(this.value, 'value', null);
if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.map(x => x.name).join('\n');
this.$watch('value.type', (t) => {
this.warn = null;
if (this.value.type === 'fn') {
const id = uuid.v4();
this.value.value = {};
Vue.set(this.value.value, 'slots', []);
Vue.set(this.value.value, 'expression', { id, type: null });
return;
}
if (this.value.type && this.value.type.startsWith('fn:')) {
const fnName = this.value.type.split(':')[1];
const fn = this.aiScript.getVarByName(fnName);
const empties = [];
for (let i = 0; i < fn.value.slots.length; i++) {
const id = uuid.v4();
empties.push({ id, type: null });
}
Vue.set(this.value, 'args', empties);
return;
}
if (isLiteralBlock(this.value)) return;
const empties = [];
for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {
const id = uuid.v4();
empties.push({ id, type: null });
}
Vue.set(this.value, 'args', empties);
for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {
const inType = funcDefs[this.value.type].in[i];
if (typeof inType !== 'number') {
if (inType === 'number') this.value.args[i].type = 'number';
if (inType === 'string') this.value.args[i].type = 'text';
}
}
});
this.$watch('value.args', (args) => {
if (args == null) {
this.warn = null;
return;
}
const emptySlotIndex = args.findIndex(x => x.type === null);
if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
this.warn = {
slot: emptySlotIndex
};
} else {
this.warn = null;
}
}, {
deep: true
});
this.$watch('aiScript.variables', () => {
if (this.type != null && this.value) {
this.error = this.aiScript.typeCheck(this.value);
}
}, {
deep: true
});
},
methods: {
async changeType() {
const { canceled, result: type } = await this.$root.dialog({
type: null,
title: this.$t('select-type'),
select: {
groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null)
},
showCancelButton: true
});
if (canceled) return;
this.value.type = type;
},
_getExpectedType(slot: number) {
return this.aiScript.getExpectedType(this.value, slot);
}
}
});
</script>
<style lang="stylus" scoped>
.turmquns
opacity 0.7
.pbglfege
opacity 0.5
padding 16px
text-align center
cursor pointer
color var(--text)
.tbwccoaw
> input
> textarea
display block
-webkit-appearance none
-moz-appearance none
appearance none
width 100%
max-width 100%
min-width 100%
border none
box-shadow none
padding 16px
font-size 16px
background transparent
color var(--text)
> textarea
min-height 100px
.hpdwcrvs
padding 16px
> select
display block
padding 4px
font-size 16px
width 100%
</style>

View File

@ -0,0 +1,473 @@
<template>
<div>
<div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
<header>
<div class="title"><fa :icon="faStickyNote"/> {{ readonly ? $t('read-page') : pageId ? $t('edit-page') : $t('new-page') }}</div>
<div class="buttons">
<button @click="del()" v-if="!readonly"><fa :icon="faTrashAlt"/></button>
<button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
<button @click="save()" v-if="!readonly"><fa :icon="faSave"/></button>
</div>
</header>
<section>
<a class="view" v-if="pageId" :href="`/@${ author.username }/pages/${ currentName }`" target="_blank"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</a>
<ui-input v-model="title">
<span>{{ $t('title') }}</span>
</ui-input>
<template v-if="showOptions">
<ui-input v-model="summary">
<span>{{ $t('summary') }}</span>
</ui-input>
<ui-input v-model="name">
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
<span>{{ $t('url') }}</span>
</ui-input>
<ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch>
<ui-select v-model="font">
<template #label>{{ $t('font') }}</template>
<option value="serif">{{ $t('fontSerif') }}</option>
<option value="sans-serif">{{ $t('fontSansSerif') }}</option>
</ui-select>
<div class="eyeCatch">
<ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catchig-image') }}</ui-button>
<div v-else-if="eyeCatchingImage">
<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
<ui-button @click="removeEyeCatchingImage()" v-if="!readonly"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catchig-image') }}</ui-button>
</div>
</div>
</template>
<x-blocks class="content" v-model="content" :ai-script="aiScript"/>
<ui-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></ui-button>
</section>
</div>
<ui-container :body-togglable="true">
<template #header><fa :icon="faMagic"/> {{ $t('variables') }}</template>
<div class="qmuvgica">
<div class="variables" v-show="variables.length > 0">
<template v-for="variable in variables">
<x-variable
:value="variable"
:removable="true"
@input="v => updateVariable(v)"
@remove="() => removeVariable(variable)"
:key="variable.name"
:ai-script="aiScript"
:name="variable.name"
:title="variable.name"
/>
</template>
</div>
<ui-button @click="addVariable()" class="add" v-if="!readonly"><fa :icon="faPlus"/></ui-button>
<ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info>
<template v-if="moreDetails">
<ui-info><span v-html="$t('variables-info2')"></span></ui-info>
<ui-info><span v-html="$t('variables-info3')"></span></ui-info>
<ui-info><span v-html="$t('variables-info4')"></span></ui-info>
</template>
</div>
</ui-container>
<ui-container :body-togglable="true" :expanded="false">
<template #header><fa :icon="faCode"/> {{ $t('inspector') }}</template>
<div style="padding:0 32px 32px 32px;">
<ui-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('content') }}</ui-textarea>
<ui-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('variables') }}</ui-textarea>
</div>
</ui-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../i18n';
import XVariable from './page-editor.script-block.vue';
import XBlocks from './page-editor.blocks.vue';
import * as uuid from 'uuid';
import { blockDefs } from '../../../../../../misc/aiscript/index';
import { ASTypeChecker } from '../../../../../../misc/aiscript/type-checker';
import { url } from '../../../../config';
import { collectPageVars } from '../../../scripts/collect-page-vars';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XVariable, XBlocks
},
props: {
page: {
type: Object,
required: false
},
readonly: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
author: this.$store.state.i,
pageId: null,
currentName: null,
title: '',
summary: null,
name: Date.now().toString(),
eyeCatchingImage: null,
eyeCatchingImageId: null,
font: 'sans-serif',
content: [],
alignCenter: false,
variables: [],
aiScript: null,
showOptions: false,
moreDetails: false,
url,
faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode
};
},
watch: {
async eyeCatchingImageId() {
if (this.eyeCatchingImageId == null) {
this.eyeCatchingImage = null;
} else {
this.eyeCatchingImage = await this.$root.api('drive/files/show', {
fileId: this.eyeCatchingImageId,
});
}
},
},
created() {
this.aiScript = new ASTypeChecker();
this.$watch('variables', () => {
this.aiScript.variables = this.variables;
}, { deep: true });
this.$watch('content', () => {
this.aiScript.pageVars = collectPageVars(this.content);
}, { deep: true });
if (this.page) {
this.author = this.page.user;
this.pageId = this.page.id;
this.title = this.page.title;
this.name = this.page.name;
this.currentName = this.page.name;
this.summary = this.page.summary;
this.font = this.page.font;
this.alignCenter = this.page.alignCenter;
this.content = this.page.content;
this.variables = this.page.variables;
this.eyeCatchingImageId = this.page.eyeCatchingImageId;
} else {
const id = uuid.v4();
this.content = [{
id,
type: 'text',
text: 'Hello World!'
}];
}
},
provide() {
return {
readonly: this.readonly,
getScriptBlockList: this.getScriptBlockList,
getPageBlockList: this.getPageBlockList
}
},
methods: {
save() {
if (this.pageId) {
this.$root.api('pages/update', {
pageId: this.pageId,
title: this.title.trim(),
name: this.name.trim(),
summary: this.summary,
font: this.font,
alignCenter: this.alignCenter,
content: this.content,
variables: this.variables,
eyeCatchingImageId: this.eyeCatchingImageId,
}).then(page => {
this.currentName = this.name.trim();
this.$root.dialog({
type: 'success',
text: this.$t('page-updated')
});
});
} else {
this.$root.api('pages/create', {
title: this.title.trim(),
name: this.name.trim(),
summary: this.summary,
font: this.font,
alignCenter: this.alignCenter,
content: this.content,
variables: this.variables,
eyeCatchingImageId: this.eyeCatchingImageId,
}).then(page => {
this.pageId = page.id;
this.currentName = this.name.trim();
this.$root.dialog({
type: 'success',
text: this.$t('page-created')
});
this.$router.push(`/i/pages/edit/${this.pageId}`);
});
}
},
del() {
this.$root.dialog({
type: 'warning',
text: this.$t('are-you-sure-delete'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('pages/delete', {
pageId: this.pageId,
}).then(() => {
this.$root.dialog({
type: 'success',
text: this.$t('page-deleted')
});
this.$router.push(`/i/pages`);
});
});
},
async add() {
const { canceled, result: type } = await this.$root.dialog({
type: null,
title: this.$t('choose-block'),
select: {
groupedItems: this.getPageBlockList()
},
showCancelButton: true
});
if (canceled) return;
const id = uuid.v4();
this.content.push({ id, type });
},
async addVariable() {
let { canceled, result: name } = await this.$root.dialog({
title: this.$t('enter-variable-name'),
input: {
type: 'text',
},
showCancelButton: true
});
if (canceled) return;
name = name.trim();
if (this.aiScript.isUsedName(name)) {
this.$root.dialog({
type: 'error',
text: this.$t('the-variable-name-is-already-used')
});
return;
}
const id = uuid.v4();
this.variables.push({ id, name, type: null });
},
removeVariable(v) {
const i = this.variables.findIndex(x => x.name === v.name);
const newValue = [
...this.variables.slice(0, i),
...this.variables.slice(i + 1)
];
this.variables = newValue;
},
getPageBlockList() {
return [{
label: this.$t('content-blocks'),
items: [
{ value: 'section', text: this.$t('blocks.section') },
{ value: 'text', text: this.$t('blocks.text') },
{ value: 'image', text: this.$t('blocks.image') },
{ value: 'textarea', text: this.$t('blocks.textarea') },
]
}, {
label: this.$t('input-blocks'),
items: [
{ value: 'button', text: this.$t('blocks.button') },
{ value: 'textInput', text: this.$t('blocks.textInput') },
{ value: 'textareaInput', text: this.$t('blocks.textareaInput') },
{ value: 'numberInput', text: this.$t('blocks.numberInput') },
{ value: 'switch', text: this.$t('blocks.switch') },
{ value: 'counter', text: this.$t('blocks.counter') }
]
}, {
label: this.$t('special-blocks'),
items: [
{ value: 'if', text: this.$t('blocks.if') },
{ value: 'post', text: this.$t('blocks.post') }
]
}];
},
getScriptBlockList(type: string = null) {
const list = [];
const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type);
for (const block of blocks) {
const category = list.find(x => x.category === block.category);
if (category) {
category.items.push({
value: block.type,
text: this.$t(`script.blocks.${block.type}`)
});
} else {
list.push({
category: block.category,
label: this.$t(`script.categories.${block.category}`),
items: [{
value: block.type,
text: this.$t(`script.blocks.${block.type}`)
}]
});
}
}
const userFns = this.variables.filter(x => x.type === 'fn');
if (userFns.length > 0) {
list.unshift({
label: this.$t(`script.categories.fn`),
items: userFns.map(v => ({
value: 'fn:' + v.name,
text: v.name
}))
});
}
return list;
},
setEyeCatchingImage() {
this.$chooseDriveFile({
multiple: false
}).then(file => {
this.eyeCatchingImageId = file.id;
});
},
removeEyeCatchingImage() {
this.eyeCatchingImageId = null;
}
}
});
</script>
<style lang="stylus" scoped>
.gwbmwxkm
overflow hidden
background var(--face)
margin-bottom 16px
&.round
border-radius 6px
&.shadow
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
> header
background var(--faceHeader)
> .title
z-index 1
margin 0
padding 0 16px
line-height 42px
font-size 0.9em
font-weight bold
color var(--faceHeaderText)
box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
> [data-icon]
margin-right 6px
&:empty
display none
> .buttons
position absolute
z-index 2
top 0
right 0
> button
padding 0
width 42px
font-size 0.9em
line-height 42px
color var(--faceTextButton)
&:hover
color var(--faceTextButtonHover)
&:active
color var(--faceTextButtonActive)
> section
padding 0 32px 32px 32px
@media (max-width 500px)
padding 0 16px 16px 16px
> .view
display inline-block
margin 16px 0 0 0
font-size 14px
> .content
margin-bottom 16px
> .eyeCatch
margin-bottom 16px
> div
> img
max-width 100%
.qmuvgica
padding 32px
@media (max-width 500px)
padding 16px
> .variables
margin-bottom 16px
> .add
margin-bottom 16px
</style>

View File

@ -0,0 +1,141 @@
<template>
<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
<article>
<header>
<h1 :title="page.title">{{ page.title }}</h1>
</header>
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
<footer>
<img class="icon" :src="page.user.avatarUrl"/>
<p>{{ page.user | userName }}</p>
</footer>
</article>
</router-link>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
page: {
type: Object,
required: true
},
},
});
</script>
<style lang="stylus" scoped>
.vhpxefrj
display block
overflow hidden
width 100%
background var(--face)
&.round
border-radius 8px
&.shadow
box-shadow 0 4px 16px rgba(#000, 0.1)
@media (min-width 500px)
box-shadow 0 8px 32px rgba(#000, 0.1)
> .thumbnail
position absolute
width 100px
height 100%
background-position center
background-size cover
display flex
justify-content center
align-items center
> button
font-size 3.5em
opacity: 0.7
&:hover
font-size 4em
opacity 0.9
& + article
left 100px
width calc(100% - 100px)
> article
padding 16px
> header
margin-bottom 8px
> h1
margin 0
font-size 1em
color var(--urlPreviewTitle)
> p
margin 0
color var(--urlPreviewText)
font-size 0.8em
> footer
margin-top 8px
height 16px
> img
display inline-block
width 16px
height 16px
margin-right 4px
vertical-align top
> p
display inline-block
margin 0
color var(--urlPreviewInfo)
font-size 0.8em
line-height 16px
vertical-align top
@media (max-width 700px)
> .thumbnail
position relative
width 100%
height 100px
& + article
left 0
width 100%
@media (max-width 550px)
font-size 12px
> .thumbnail
height 80px
> article
padding 12px
@media (max-width 500px)
font-size 10px
> .thumbnail
height 70px
> article
padding 8px
> header
margin-bottom 4px
> footer
margin-top 4px
> img
width 12px
height 12px
</style>

View File

@ -14,7 +14,7 @@
<section>
<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
<ui-input v-model="endpoint" :datalist="endpoints">
<ui-input v-model="endpoint" :datalist="endpoints" @change="onEndpointChange()">
<span>{{ $t('console.endpoint') }}</span>
</ui-input>
<ui-textarea v-model="body">
@ -80,6 +80,22 @@ export default Vue.extend({
this.sending = false;
this.res = JSON5.stringify(err, null, 2);
});
},
onEndpointChange() {
this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => {
const body = {};
for (const p of endpoint.params) {
body[p.name] =
p.type === 'String' ? '' :
p.type === 'Number' ? 0 :
p.type === 'Boolean' ? false :
p.type === 'Array' ? [] :
p.type === 'Object' ? {} :
null;
}
this.body = JSON5.stringify(body, null, 2);
});
}
}
});

View File

@ -290,12 +290,17 @@ export default Vue.extend({
this.exportTarget == 'mute' ? 'i/export-mute' :
this.exportTarget == 'blocking' ? 'i/export-blocking' :
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
null, {});
this.$root.dialog({
type: 'info',
text: this.$t('export-requested')
});
null, {}).then(() => {
this.$root.dialog({
type: 'info',
text: this.$t('export-requested')
});
}).catch((e: any) => {
this.$root.dialog({
type: 'error',
text: e.message
});
});
},
doImport() {

View File

@ -1,7 +1,7 @@
<template>
<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr"
:is="link ? 'a' : 'button'"
:class="{ inline, primary, wait }"
:class="{ inline, primary, wait, round: $store.state.device.roundedCorners }"
:type="type"
@click="$emit('click')"
@mousedown="onMousedown"
@ -116,7 +116,6 @@ export default Vue.extend({
font-size 16px
line-height 24px
border none
border-radius 6px
outline none
box-shadow none
text-decoration none
@ -124,6 +123,9 @@ export default Vue.extend({
color var(--text)
background var(--buttonBg)
&.round
border-radius 6px
&:not(:disabled):hover
background var(--buttonHoverBg)
@ -157,7 +159,9 @@ export default Vue.extend({
bottom -5px
left -5px
border 2px solid var(--primaryAlpha03)
border-radius 10px
&.round:focus:after
border-radius 10px
&:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr
margin-top 16px
@ -197,7 +201,6 @@ export default Vue.extend({
left 0
width 100%
height 100%
border-radius 6px
overflow hidden
>>> div
@ -210,6 +213,9 @@ export default Vue.extend({
transform scale(1)
transition all 0.5s cubic-bezier(0, .5, .5, 1)
&.round > .ripples
border-radius 6px
&.primary > .ripples >>> div
background rgba(0, 0, 0, 0.15)

View File

@ -23,6 +23,7 @@
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@change="$emit('change', $event)"
:list="id"
>
<input v-else ref="input"
@ -38,6 +39,7 @@
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
@change="$emit('change', $event)"
:list="id"
>
<datalist :id="id" v-if="datalist">
@ -60,7 +62,7 @@
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
</div>
<div class="toggle" v-if="withPasswordToggle">
<a @click='togglePassword'>
<a @click="togglePassword">
<span v-if="type == 'password'"><fa :icon="['fa', 'eye']"/> {{ $t('@.show-password') }}</span>
<span v-if="type != 'password'"><fa :icon="['far', 'eye-slash']"/> {{ $t('@.hide-password') }}</span>
</a>
@ -182,7 +184,11 @@ export default Vue.extend({
this.v = v;
},
v(v) {
this.$emit('input', v);
if (this.type === 'number') {
this.$emit('input', parseInt(v, 10));
} else {
this.$emit('input', v);
}
if (this.withPasswordMeter) {
if (v == '') {

View File

@ -0,0 +1,40 @@
<template>
<component :is="'x-' + value.type" :value="value" :page="page" :script="script" :key="value.id" :h="h"/>
</template>
<script lang="ts">
import Vue from 'vue';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
import XButton from './page.button.vue';
import XNumberInput from './page.number-input.vue';
import XTextInput from './page.text-input.vue';
import XTextareaInput from './page.textarea-input.vue';
import XSwitch from './page.switch.vue';
import XIf from './page.if.vue';
import XTextarea from './page.textarea.vue';
import XPost from './page.post.vue';
import XCounter from './page.counter.vue';
export default Vue.extend({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter
},
props: {
value: {
required: true
},
script: {
required: true
},
page: {
required: true
},
h: {
required: true
}
},
});
</script>

View File

@ -0,0 +1,42 @@
<template>
<div>
<ui-button class="kudkigyw" @click="click()">{{ script.interpolate(value.text) }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
methods: {
click() {
if (this.value.action === 'dialog') {
this.script.eval();
this.$root.dialog({
text: this.script.interpolate(this.value.content)
});
} else if (this.value.action === 'resetRandom') {
this.script.aiScript.updateRandomSeed(Math.random());
this.script.eval();
}
}
}
});
</script>
<style lang="stylus" scoped>
.kudkigyw
display inline-block
min-width 300px
max-width 450px
margin 8px 0
</style>

View File

@ -0,0 +1,47 @@
<template>
<div>
<ui-button class="llumlmnx" @click="click()">{{ script.interpolate(value.text) }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: 0,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.eval();
}
},
methods: {
click() {
this.v = this.v + (this.value.inc || 1);
}
}
});
</script>
<style lang="stylus" scoped>
.llumlmnx
display inline-block
min-width 300px
max-width 450px
margin 8px 0
</style>

View File

@ -0,0 +1,30 @@
<template>
<div v-show="script.vars[value.var]">
<x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
},
page: {
required: true
},
h: {
required: true
}
},
beforeCreate() {
this.$options.components.XBlock = require('./page.block.vue').default
},
});
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="lzyxtsnt">
<img v-if="image" :src="image.url"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
page: {
required: true
},
},
data() {
return {
image: null,
};
},
created() {
this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
}
});
</script>
<style lang="stylus" scoped>
.lzyxtsnt
> img
max-width 100%
</style>

View File

@ -0,0 +1,41 @@
<template>
<div>
<ui-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</ui-input>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.eval();
}
}
});
</script>
<style lang="stylus" scoped>
.kudkigyw
display inline-block
min-width 300px
max-width 450px
margin 8px 0
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="ngbfujlo">
<ui-textarea class="textarea" :value="text" readonly></ui-textarea>
<ui-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
export default Vue.extend({
i18n: i18n('pages'),
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
text: this.script.interpolate(this.value.text),
posted: false,
posting: false,
};
},
created() {
this.$watch('script.vars', () => {
this.text = this.script.interpolate(this.value.text);
}, { deep: true });
},
methods: {
post() {
this.posting = true;
this.$root.api('notes/create', {
text: this.text,
}).then(() => {
this.posted = true;
this.$root.dialog({
type: 'success',
splash: true
});
});
}
}
});
</script>
<style lang="stylus" scoped>
.ngbfujlo
padding 0 32px 32px 32px
border solid 2px var(--pageBlockBorder)
border-radius 6px
@media (max-width 600px)
padding 0 16px 16px 16px
> .textarea
margin-top 16px
margin-bottom 16px
</style>

View File

@ -0,0 +1,55 @@
<template>
<section class="sdgxphyu">
<component :is="'h' + h">{{ value.title }}</component>
<div class="children">
<x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h + 1"/>
</div>
</section>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
},
page: {
required: true
},
h: {
required: true
}
},
beforeCreate() {
this.$options.components.XBlock = require('./page.block.vue').default
},
});
</script>
<style lang="stylus" scoped>
.sdgxphyu
margin 1.5em 0
> h2
font-size 1.35em
margin 0 0 0.5em 0
> h3
font-size 1em
margin 0 0 0.5em 0
> h4
font-size 1em
margin 0 0 0.5em 0
> .children
//padding 16px
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="hkcxmtwj">
<ui-switch v-model="v">{{ script.interpolate(value.text) }}</ui-switch>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.eval();
}
}
});
</script>
<style lang="stylus" scoped>
.hkcxmtwj
display inline-block
margin 16px auto
& + .hkcxmtwj
margin-left 16px
</style>

View File

@ -0,0 +1,41 @@
<template>
<div>
<ui-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</ui-input>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.eval();
}
}
});
</script>
<style lang="stylus" scoped>
.kudkigyw
display inline-block
min-width 300px
max-width 450px
margin 8px 0
</style>

View File

@ -0,0 +1,35 @@
<template>
<div class="">
<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
text: this.script.interpolate(this.value.text),
};
},
created() {
this.$watch('script.vars', () => {
this.text = this.script.interpolate(this.value.text);
}, { deep: true });
}
});
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,36 @@
<template>
<div>
<ui-textarea class="" v-model="v">{{ script.interpolate(value.text) }}</ui-textarea>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.eval();
}
}
});
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,33 @@
<template>
<ui-textarea class="" :value="text" readonly></ui-textarea>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
text: this.script.interpolate(this.value.text),
};
},
created() {
this.$watch('script.vars', () => {
this.text = this.script.interpolate(this.value.text);
}, { deep: true });
}
});
</script>
<style lang="stylus" scoped>
</style>

View File

@ -0,0 +1,164 @@
<template>
<div v-if="page" class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }">
<header>
<div class="title">{{ page.title }}</div>
</header>
<div v-if="script">
<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/>
</div>
<footer>
<small>@{{ page.user.username }}</small>
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
</footer>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import { faICursor, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue';
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
import { collectPageVars } from '../../../scripts/collect-page-vars';
import { url } from '../../../../config';
class Script {
public aiScript: ASEvaluator;
private onError: any;
public vars: Record<string, any>;
constructor(aiScript, onError) {
this.aiScript = aiScript;
this.onError = onError;
this.eval();
}
public eval() {
try {
this.vars = this.aiScript.evaluateVars();
} catch (e) {
this.onError(e);
}
}
public interpolate(str: string) {
if (str == null) return null;
return str.replace(/\{(.+?)\}/g, match => {
const v = this.vars[match.slice(1, -1).trim()];
return v == null ? 'NULL' : v.toString();
});
}
}
export default Vue.extend({
i18n: i18n('pages'),
components: {
XBlock
},
props: {
pageName: {
type: String,
required: true
},
username: {
type: String,
required: true
},
},
data() {
return {
page: null,
script: null,
faPlus, faICursor, faSave, faStickyNote
};
},
created() {
this.$root.api('pages/show', {
name: this.pageName,
username: this.username,
}).then(page => {
this.page = page;
const pageVars = this.getPageVars();
this.script = new Script(new ASEvaluator(this.page.variables, pageVars, {
randomSeed: Math.random(),
user: page.user,
visitor: this.$store.state.i,
page: page,
url: url
}), e => {
console.dir(e);
});
});
},
methods: {
getPageVars() {
return collectPageVars(this.page.content);
},
}
});
</script>
<style lang="stylus" scoped>
.iroscrza
overflow hidden
background var(--face)
&.center
text-align center
&.round
border-radius 6px
&.shadow
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
> header
> .title
z-index 1
margin 0
padding 32px 64px
font-size 24px
font-weight bold
color var(--text)
box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
@media (max-width 600px)
padding 16px 32px
font-size 20px
> div
color var(--text)
padding 48px 64px
font-size 18px
@media (max-width 600px)
padding 24px 32px
font-size 16px
> footer
color var(--text)
padding 0 64px 38px 64px
@media (max-width 600px)
padding 0 32px 28px 32px
> small
display block
opacity 0.5
> a
font-size 14px
> a + a
margin-left 8px
</style>

View File

@ -156,7 +156,12 @@ init(async (launch, os) => {
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) },
]},
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },

View File

@ -9,35 +9,42 @@
<ul>
<li>
<router-link :to="`/@${ $store.state.i.username }`">
<i><fa icon="user"/></i>
<i><fa icon="user" fixed-width/></i>
<span>{{ $t('profile') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li @click="drive">
<p>
<i><fa icon="cloud"/></i>
<i><fa icon="cloud" fixed-width/></i>
<span>{{ $t('@.drive') }}</span>
<i><fa icon="angle-right"/></i>
</p>
</li>
<li>
<router-link to="/i/favorites">
<i><fa icon="star"/></i>
<i><fa icon="star" fixed-width/></i>
<span>{{ $t('@.favorites') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li @click="list">
<p>
<i><fa icon="list"/></i>
<i><fa icon="list" fixed-width/></i>
<span>{{ $t('lists') }}</span>
<i><fa icon="angle-right"/></i>
</p>
</li>
<li>
<router-link to="/i/pages">
<i><fa :icon="faStickyNote" fixed-width/></i>
<span>{{ $t('@.pages') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
<p>
<i><fa :icon="['far', 'envelope']"/></i>
<i><fa :icon="['far', 'envelope']" fixed-width/></i>
<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
<i><fa icon="angle-right"/></i>
</p>
@ -46,14 +53,14 @@
<ul>
<li>
<router-link to="/i/settings">
<i><fa icon="cog"/></i>
<i><fa icon="cog" fixed-width/></i>
<span>{{ $t('@.settings') }}</span>
<i><fa icon="angle-right"/></i>
</router-link>
</li>
<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
<a href="/admin">
<i><fa icon="terminal"/></i>
<i><fa icon="terminal" fixed-width/></i>
<span>{{ $t('admin') }}</span>
<i><fa icon="angle-right"/></i>
</a>
@ -76,7 +83,7 @@
<ul>
<li @click="signout">
<p class="signout">
<i><fa icon="power-off"/></i>
<i><fa icon="power-off" fixed-width/></i>
<span>{{ $t('@.signout') }}</span>
</p>
</li>
@ -95,14 +102,14 @@ import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('desktop/views/components/ui.header.account.vue'),
data() {
return {
isOpen: false,
faHome, faColumns, faMoon, faSun
faHome, faColumns, faMoon, faSun, faStickyNote
};
},
computed: {

View File

@ -0,0 +1,92 @@
<template>
<div class="rknalgpo" v-if="!fetching">
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
<sequential-entrance animation="entranceFromTop" delay="25">
<template v-for="page in pages">
<x-page-preview class="page" :page="page" :key="page.id"/>
</template>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XPagePreview from '../../../common/views/components/page-preview.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XPagePreview
},
data() {
return {
fetching: true,
pages: [],
existMore: false,
moreFetching: false,
faStickyNote, faPlus
};
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
this.$root.api('i/pages', {
limit: 11
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
}
this.pages = pages;
this.fetching = false;
Progress.done();
});
},
fetchMore() {
this.moreFetching = true;
this.$root.api('i/pages', {
limit: 11,
untilId: this.pages[this.pages.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
} else {
this.existMore = false;
}
this.pages = this.pages.concat(pages);
this.moreFetching = false;
});
},
create() {
this.$router.push(`/i/pages/new`);
}
}
});
</script>
<style lang="stylus" scoped>
.rknalgpo
margin 0 auto
> * > .page
margin-bottom 8px
@media (min-width 500px)
> * > .page
margin-bottom 16px
</style>

View File

@ -0,0 +1,67 @@
<template>
<mk-ui>
<main>
<x-page-editor v-if="page !== undefined" :page="page" :readonly="readonly"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
components: {
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
},
props: {
pageId: {
type: String,
required: false
},
pageName: {
type: String,
required: false
},
user: {
type: String,
required: false
}
},
data() {
return {
page: undefined,
readonly: false
};
},
created() {
if (this.pageId) {
this.$root.api('pages/show', {
pageId: this.pageId,
}).then(page => {
this.page = page;
});
} else if (this.pageName && this.user) {
this.$root.api('pages/show', {
name: this.pageName,
username: this.user,
}).then(page => {
this.readonly = true;
this.page = page;
});
} else {
this.page = null;
}
}
});
</script>
<style lang="stylus" scoped>
main
margin 0 auto
padding 16px
max-width 900px
</style>

View File

@ -0,0 +1,36 @@
<template>
<mk-ui>
<main>
<x-page :page-name="page" :username="user"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
components: {
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
},
props: {
page: {
type: String,
required: true
},
user: {
type: String,
required: true
},
}
});
</script>
<style lang="stylus" scoped>
main
margin 0 auto
padding 16px
max-width 950px
</style>

View File

@ -173,9 +173,10 @@ export default class MiOS extends EventEmitter {
// Init service worker
if (this.shouldRegisterSw) {
this.getMeta().then(data => {
this.registerSw(data.swPublickey);
});
// #4813
//this.getMeta().then(data => {
// this.registerSw(data.swPublickey);
//});
}
};
@ -195,7 +196,7 @@ export default class MiOS extends EventEmitter {
});
} else {
// Get token from cookie or localStorage
const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1] || localStorage.getItem('i');
const i = (document.cookie.match(/i=(\w+)/) || [null, null])[1] || localStorage.getItem('i');
fetchme(i, me => {
if (me) {

View File

@ -135,6 +135,7 @@ init((launch, os) => {
{ path: '/signup', name: 'signup', component: MkSignup },
{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) },
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
@ -144,6 +145,8 @@ init((launch, os) => {
{ path: '/i/drive', name: 'drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
{ path: '/i/drive/file/:file', component: MkDrive },
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
@ -156,6 +159,8 @@ init((launch, os) => {
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
]},
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
{ path: '/notes/:note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow },
{ path: '*', component: MkNotFound }

View File

@ -29,6 +29,7 @@
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li>
</ul>
<ul>
<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
@ -66,7 +67,7 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import { lang } from '../../../config';
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
import { search } from '../../../common/scripts/search';
export default Vue.extend({
@ -86,7 +87,7 @@ export default Vue.extend({
announcements: [],
searching: false,
showNotifications: false,
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote
};
},

View File

@ -0,0 +1,67 @@
<template>
<mk-ui>
<main>
<x-page-editor v-if="page !== undefined" :page="page" :readonly="readonly"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
components: {
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
},
props: {
pageId: {
type: String,
required: false
},
pageName: {
type: String,
required: false
},
user: {
type: String,
required: false
}
},
data() {
return {
page: undefined,
readonly: false
};
},
created() {
if (this.pageId) {
this.$root.api('pages/show', {
pageId: this.pageId,
}).then(page => {
this.page = page;
});
} else if (this.pageName && this.user) {
this.$root.api('pages/show', {
name: this.pageName,
username: this.user,
}).then(page => {
this.readonly = true;
this.page = page;
});
} else {
this.page = null;
}
}
});
</script>
<style lang="stylus" scoped>
main
margin 0 auto
padding 16px
max-width 1000px
</style>

View File

@ -0,0 +1,39 @@
<template>
<mk-ui>
<main>
<x-page :page-name="page" :username="user"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
components: {
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
},
props: {
page: {
type: String,
required: true
},
user: {
type: String,
required: true
},
}
});
</script>
<style lang="stylus" scoped>
main
margin 0 auto
padding 16px
max-width 1000px
@media (min-width 600px)
padding 32px
</style>

View File

@ -0,0 +1,94 @@
<template>
<mk-ui>
<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
<main>
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
<sequential-entrance animation="entranceFromTop" delay="25">
<template v-for="page in pages">
<x-page-preview class="page" :page="page" :key="page.id"/>
</template>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import Progress from '../../../common/scripts/loading';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XPagePreview from '../../../common/views/components/page-preview.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XPagePreview
},
data() {
return {
fetching: true,
pages: [],
existMore: false,
moreFetching: false,
faStickyNote, faPlus
};
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.fetching = true;
this.$root.api('i/pages', {
limit: 11
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
}
this.pages = pages;
this.fetching = false;
Progress.done();
});
},
fetchMore() {
this.moreFetching = true;
this.$root.api('i/pages', {
limit: 11,
untilId: this.pages[this.pages.length - 1].id
}).then(pages => {
if (pages.length == 11) {
this.existMore = true;
pages.pop();
} else {
this.existMore = false;
}
this.pages = this.pages.concat(pages);
this.moreFetching = false;
});
},
create() {
this.$router.push(`/i/pages/new`);
}
}
});
</script>
<style lang="stylus" scoped>
main
> * > .page
margin-bottom 8px
@media (min-width 500px)
> * > .page
margin-bottom 16px
</style>

View File

@ -3,12 +3,6 @@
*/
import composeNotification from './common/scripts/compose-notification';
import { erase } from '../../prelude/array';
// キャッシュするリソース
const cachee = [
'/'
];
// インストールされたとき
self.addEventListener('install', ev => {
@ -16,31 +10,9 @@ self.addEventListener('install', ev => {
ev.waitUntil(Promise.all([
self.skipWaiting(), // Force activate
caches.open(_VERSION_).then(cache => cache.addAll(cachee)) // Cache
]));
});
// アクティベートされたとき
self.addEventListener('activate', ev => {
// Clean up old caches
ev.waitUntil(
caches.keys().then(keys => Promise.all(
erase(_VERSION_, keys)
.map(key => caches.delete(key))
))
);
});
// リクエストが発生したとき
self.addEventListener('fetch', ev => {
ev.respondWith(
// キャッシュがあるか確認してあればそれを返す
caches.match(ev.request).then(response =>
response || fetch(ev.request)
)
);
});
// プッシュ通知を受け取ったとき
self.addEventListener('push', ev => {
// クライアント取得
@ -59,11 +31,3 @@ self.addEventListener('push', ev => {
});
}));
});
self.addEventListener('message', ev => {
if (ev.data == 'clear') {
caches.keys().then(keys => {
for (const key of keys) caches.delete(key);
});
}
});

View File

@ -232,5 +232,8 @@
adminDashboardCardBg: '$secondary',
adminDashboardCardFg: '$text',
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
},
}

View File

@ -232,5 +232,8 @@
adminDashboardCardBg: '$secondary',
adminDashboardCardFg: '$text',
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
},
}

View File

@ -40,6 +40,7 @@ import { Poll } from '../models/entities/poll';
import { UserKeypair } from '../models/entities/user-keypair';
import { UserPublickey } from '../models/entities/user-publickey';
import { UserProfile } from '../models/entities/user-profile';
import { Page } from '../models/entities/page';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@ -114,6 +115,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
NoteReaction,
NoteWatching,
NoteUnread,
Page,
Log,
DriveFile,
DriveFolder,

View File

@ -0,0 +1,241 @@
import autobind from 'autobind-decorator';
import * as seedrandom from 'seedrandom';
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
type Fn = {
slots: string[];
exec: (args: Record<string, any>) => ReturnType<ASEvaluator['evaluate']>;
};
/**
* AiScript evaluator
*/
export class ASEvaluator {
private variables: Variable[];
private pageVars: PageVar[];
private envVars: Record<keyof typeof envVarsDef, any>;
private opts: {
randomSeed: string; user?: any; visitor?: any; page?: any; url?: string; version: string;
};
constructor(variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) {
this.variables = variables;
this.pageVars = pageVars;
this.opts = opts;
const date = new Date();
this.envVars = {
AI: 'kawaii',
VERSION: opts.version,
URL: opts.page ? `${opts.url}/@${opts.page.user.username}/pages/${opts.page.name}` : '',
LOGIN: opts.visitor != null,
NAME: opts.visitor ? opts.visitor.name : '',
USERNAME: opts.visitor ? opts.visitor.username : '',
USERID: opts.visitor ? opts.visitor.id : '',
NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
IS_CAT: opts.visitor ? opts.visitor.isCat : false,
MY_NOTES_COUNT: opts.user ? opts.user.notesCount : 0,
MY_FOLLOWERS_COUNT: opts.user ? opts.user.followersCount : 0,
MY_FOLLOWING_COUNT: opts.user ? opts.user.followingCount : 0,
SEED: opts.randomSeed ? opts.randomSeed : '',
YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
};
}
@autobind
public updatePageVar(name: string, value: any) {
const pageVar = this.pageVars.find(v => v.name === name);
if (pageVar !== undefined) {
pageVar.value = value;
} else {
throw new AiScriptError(`No such page var '${name}'`);
}
}
@autobind
public updateRandomSeed(seed: string) {
this.opts.randomSeed = seed;
this.envVars.SEED = seed;
}
@autobind
private interpolate(str: string, scope: Scope) {
return str.replace(/\{(.+?)\}/g, match => {
const v = scope.getState(match.slice(1, -1).trim());
return v == null ? 'NULL' : v.toString();
});
}
@autobind
public evaluateVars(): Record<string, any> {
const values: Record<string, any> = {};
for (const [k, v] of Object.entries(this.envVars)) {
values[k] = v;
}
for (const v of this.pageVars) {
values[v.name] = v.value;
}
for (const v of this.variables) {
values[v.name] = this.evaluate(v, new Scope([values]));
}
return values;
}
@autobind
private evaluate(block: Block, scope: Scope): any {
if (block.type === null) {
return null;
}
if (block.type === 'number') {
return parseInt(block.value, 10);
}
if (block.type === 'text' || block.type === 'multiLineText') {
return this.interpolate(block.value || '', scope);
}
if (block.type === 'textList') {
return block.value.trim().split('\n');
}
if (block.type === 'ref') {
return scope.getState(block.value);
}
if (isFnBlock(block)) { // ユーザー関数定義
return {
slots: block.value.slots.map(x => x.name),
exec: (slotArg: Record<string, any>) => {
return this.evaluate(block.value.expression, scope.createChildScope(slotArg, block.id));
}
} as Fn;
}
if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
const fnName = block.type.split(':')[1];
const fn = scope.getState(fnName);
const args = {} as Record<string, any>;
for (let i = 0; i < fn.slots.length; i++) {
const name = fn.slots[i];
args[name] = this.evaluate(block.args[i], scope);
}
return fn.exec(args);
}
if (block.args === undefined) return null;
const date = new Date();
const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
const funcs: { [p in keyof typeof funcDefs]: Function } = {
not: (a: boolean) => !a,
or: (a: boolean, b: boolean) => a || b,
and: (a: boolean, b: boolean) => a && b,
eq: (a: any, b: any) => a === b,
notEq: (a: any, b: any) => a !== b,
gt: (a: number, b: number) => a > b,
lt: (a: number, b: number) => a < b,
gtEq: (a: number, b: number) => a >= b,
ltEq: (a: number, b: number) => a <= b,
if: (bool: boolean, a: any, b: any) => bool ? a : b,
for: (times: number, fn: Fn) => {
const result = [];
for (let i = 0; i < times; i++) {
result.push(fn.exec({
[fn.slots[0]]: i + 1
}));
}
return result;
},
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
remind: (a: number, b: number) => a % b,
strLen: (a: string) => a.length,
strPick: (a: string, b: number) => a[b - 1],
strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
strReverse: (a: string) => a.split('').reverse().join(''),
join: (texts: string[], separator: string) => texts.join(separator || ''),
stringToNumber: (a: string) => parseInt(a),
numberToString: (a: number) => a.toString(),
splitStrByLine: (a: string) => a.split('\n'),
random: (probability: number) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability,
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)),
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)],
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
};
const fnName = block.type;
const fn = (funcs as any)[fnName];
if (fn == null) {
throw new AiScriptError(`No such function '${fnName}'`);
} else {
return fn(...block.args.map(x => this.evaluate(x, scope)));
}
}
}
class AiScriptError extends Error {
public info?: any;
constructor(message: string, info?: any) {
super(message);
this.info = info;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AiScriptError);
}
}
}
class Scope {
private layerdStates: Record<string, any>[];
public name: string;
constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) {
this.layerdStates = layerdStates;
this.name = name || 'anonymous';
}
@autobind
public createChildScope(states: Record<string, any>, name?: Scope['name']): Scope {
const layer = [states, ...this.layerdStates];
return new Scope(layer, name);
}
/**
* 指定した名前の変数の値を取得します
* @param name 変数名
*/
@autobind
public getState(name: string): any {
for (const later of this.layerdStates) {
const state = later[name];
if (state !== undefined) {
return state;
}
}
throw new AiScriptError(
`No such variable '${name}' in scope '${this.name}'`, {
scope: this.layerdStates
});
}
}

133
src/misc/aiscript/index.ts Normal file
View File

@ -0,0 +1,133 @@
/**
* AiScript
*/
import {
faMagic,
faSquareRootAlt,
faAlignLeft,
faShareAlt,
faPlus,
faMinus,
faTimes,
faDivide,
faList,
faQuoteRight,
faEquals,
faGreaterThan,
faLessThan,
faGreaterThanEqual,
faLessThanEqual,
faNotEqual,
faDice,
faSortNumericUp,
faExchangeAlt,
faRecycle,
} from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
export type Block<V = any> = {
id: string;
type: string;
args: Block[];
value: V;
};
export type FnBlock = Block<{
slots: {
name: string;
type: Type;
}[];
expression: Block;
}>;
export type Variable = Block & {
name: string;
};
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, },
for: { in: ['number', 'function'], out: null, category: 'flow', icon: faRecycle, },
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faPlus, },
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faMinus, },
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, },
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
remind: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, },
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThan, },
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, },
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThanEqual, },
strLen: { in: ['string'], out: 'number', category: 'text', icon: faQuoteRight, },
strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: faQuoteRight, },
strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
strReverse: { in: ['string'], out: 'string', category: 'text', icon: faQuoteRight, },
join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: faQuoteRight, },
stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: faExchangeAlt, },
numberToString: { in: ['number'], out: 'string', category: 'convert', icon: faExchangeAlt, },
splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: faExchangeAlt, },
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: faDice, },
random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: faDice, },
randomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: faDice, },
};
export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
text: { out: 'string', category: 'value', icon: faQuoteRight, },
multiLineText: { out: 'string', category: 'value', icon: faAlignLeft, },
textList: { out: 'stringArray', category: 'value', icon: faList, },
number: { out: 'number', category: 'value', icon: faSortNumericUp, },
ref: { out: null, category: 'value', icon: faMagic, },
fn: { out: 'function', category: 'value', icon: faSquareRootAlt, },
};
export const blockDefs = [
...Object.entries(literalDefs).map(([k, v]) => ({
type: k, out: v.out, category: v.category, icon: v.icon
})),
...Object.entries(funcDefs).map(([k, v]) => ({
type: k, out: v.out, category: v.category, icon: v.icon
}))
];
export function isFnBlock(block: Block): block is FnBlock {
return block.type === 'fn';
}
export type PageVar = { name: string; value: any; type: Type; };
export const envVarsDef: Record<string, Type> = {
AI: 'string',
URL: 'string',
VERSION: 'string',
LOGIN: 'boolean',
NAME: 'string',
USERNAME: 'string',
USERID: 'string',
NOTES_COUNT: 'number',
FOLLOWERS_COUNT: 'number',
FOLLOWING_COUNT: 'number',
IS_CAT: 'boolean',
MY_NOTES_COUNT: 'number',
MY_FOLLOWERS_COUNT: 'number',
MY_FOLLOWING_COUNT: 'number',
SEED: null,
YMD: 'string',
};
export function isLiteralBlock(v: Block) {
if (v.type === null) return true;
if (literalDefs[v.type]) return true;
return false;
}

View File

@ -0,0 +1,186 @@
import autobind from 'autobind-decorator';
import { Type, Block, funcDefs, envVarsDef, Variable, PageVar, isLiteralBlock } from '.';
type TypeError = {
arg: number;
expect: Type;
actual: Type;
};
/**
* AiScript type checker
*/
export class ASTypeChecker {
public variables: Variable[];
public pageVars: PageVar[];
constructor(variables: ASTypeChecker['variables'] = [], pageVars: ASTypeChecker['pageVars'] = []) {
this.variables = variables;
this.pageVars = pageVars;
}
@autobind
public typeCheck(v: Block): TypeError | null {
if (isLiteralBlock(v)) return null;
const def = funcDefs[v.type];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
const generic: Type[] = [];
for (let i = 0; i < def.in.length; i++) {
const arg = def.in[i];
const type = this.infer(v.args[i]);
if (type === null) continue;
if (typeof arg === 'number') {
if (generic[arg] === undefined) {
generic[arg] = type;
} else if (type !== generic[arg]) {
return {
arg: i,
expect: generic[arg],
actual: type
};
}
} else if (type !== arg) {
return {
arg: i,
expect: arg,
actual: type
};
}
}
return null;
}
@autobind
public getExpectedType(v: Block, slot: number): Type {
const def = funcDefs[v.type];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
const generic: Type[] = [];
for (let i = 0; i < def.in.length; i++) {
const arg = def.in[i];
const type = this.infer(v.args[i]);
if (type === null) continue;
if (typeof arg === 'number') {
if (generic[arg] === undefined) {
generic[arg] = type;
}
}
}
if (typeof def.in[slot] === 'number') {
return generic[def.in[slot]] || null;
} else {
return def.in[slot];
}
}
@autobind
public infer(v: Block): Type {
if (v.type === null) return null;
if (v.type === 'text') return 'string';
if (v.type === 'multiLineText') return 'string';
if (v.type === 'textList') return 'stringArray';
if (v.type === 'number') return 'number';
if (v.type === 'ref') {
const variable = this.variables.find(va => va.name === v.value);
if (variable) {
return this.infer(variable);
}
const pageVar = this.pageVars.find(va => va.name === v.value);
if (pageVar) {
return pageVar.type;
}
const envVar = envVarsDef[v.value];
if (envVar !== undefined) {
return envVar;
}
return null;
}
if (v.type === 'fn') return null; // todo
if (v.type.startsWith('fn:')) return null; // todo
const generic: Type[] = [];
const def = funcDefs[v.type];
for (let i = 0; i < def.in.length; i++) {
const arg = def.in[i];
if (typeof arg === 'number') {
const type = this.infer(v.args[i]);
if (generic[arg] === undefined) {
generic[arg] = type;
} else {
if (type !== generic[arg]) {
generic[arg] = null;
}
}
}
}
if (typeof def.out === 'number') {
return generic[def.out];
} else {
return def.out;
}
}
@autobind
public getVarByName(name: string): Variable {
const v = this.variables.find(x => x.name === name);
if (v !== undefined) {
return v;
} else {
throw new Error(`No such variable '${name}'`);
}
}
@autobind
public getVarsByType(type: Type): Variable[] {
if (type == null) return this.variables;
return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type));
}
@autobind
public getEnvVarsByType(type: Type): string[] {
if (type == null) return Object.keys(envVarsDef);
return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k);
}
@autobind
public getPageVarsByType(type: Type): string[] {
if (type == null) return this.pageVars.map(v => v.name);
return this.pageVars.filter(v => type === v.type).map(v => v.name);
}
@autobind
public isUsedName(name: string) {
if (this.variables.some(v => v.name === name)) {
return true;
}
if (this.pageVars.some(v => v.name === name)) {
return true;
}
if (envVarsDef[name]) {
return true;
}
return false;
}
}

View File

@ -71,7 +71,7 @@ export class Instance {
/**
* ドライブ使用量
*/
@Column('integer', {
@Column('bigint', {
default: 0,
})
public driveUsage: number;

105
src/models/entities/page.ts Normal file
View File

@ -0,0 +1,105 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
import { DriveFile } from './drive-file';
@Entity()
@Index(['userId', 'name'], { unique: true })
export class Page {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the Page.'
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The updated date of the Page.'
})
public updatedAt: Date;
@Column('varchar', {
length: 256,
})
public title: string;
@Index()
@Column('varchar', {
length: 256,
})
public name: string;
@Column('varchar', {
length: 256, nullable: true
})
public summary: string | null;
@Column('boolean')
public alignCenter: boolean;
@Column('varchar', {
length: 32,
})
public font: string;
@Index()
@Column({
...id(),
comment: 'The ID of author.'
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column({
...id(),
nullable: true,
})
public eyeCatchingImageId: DriveFile['id'] | null;
@ManyToOne(type => DriveFile, {
onDelete: 'CASCADE'
})
@JoinColumn()
public eyeCatchingImage: DriveFile | null;
@Column('jsonb', {
default: []
})
public content: Record<string, any>[];
@Column('jsonb', {
default: []
})
public variables: Record<string, any>[];
/**
* public ... 公開
* followers ... フォロワーのみ
* specified ... visibleUserIds で指定したユーザーのみ
*/
@Column('enum', { enum: ['public', 'followers', 'specified'] })
public visibility: 'public' | 'followers' | 'specified';
@Index()
@Column({
...id(),
array: true, default: '{}'
})
public visibleUserIds: User['id'][];
constructor(data: Partial<Page>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -144,10 +144,10 @@ export class UserProfile {
})
public githubAccessToken: string | null;
@Column('integer', {
nullable: true, default: null,
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubId: number | null;
public githubId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
@ -169,10 +169,10 @@ export class UserProfile {
})
public discordRefreshToken: string | null;
@Column('integer', {
nullable: true, default: null,
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordExpiresDate: number | null;
public discordExpiresDate: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,

View File

@ -7,7 +7,6 @@ import { Meta } from './entities/meta';
import { SwSubscription } from './entities/sw-subscription';
import { NoteWatching } from './entities/note-watching';
import { UserListJoining } from './entities/user-list-joining';
import { Hashtag } from './entities/hashtag';
import { NoteUnread } from './entities/note-unread';
import { RegistrationTicket } from './entities/registration-tickets';
import { UserRepository } from './repositories/user';
@ -35,6 +34,8 @@ import { FollowingRepository } from './repositories/following';
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
import { AuthSessionRepository } from './repositories/auth-session';
import { UserProfile } from './entities/user-profile';
import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page';
export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
@ -62,7 +63,7 @@ export const Metas = getRepository(Meta);
export const Mutings = getCustomRepository(MutingRepository);
export const Blockings = getCustomRepository(BlockingRepository);
export const SwSubscriptions = getRepository(SwSubscription);
export const Hashtags = getRepository(Hashtag);
export const Hashtags = getCustomRepository(HashtagRepository);
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
export const RegistrationTickets = getRepository(RegistrationTicket);
export const AuthSessions = getCustomRepository(AuthSessionRepository);
@ -72,3 +73,4 @@ export const MessagingMessages = getCustomRepository(MessagingMessageRepository)
export const ReversiGames = getCustomRepository(ReversiGameRepository);
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Logs = getRepository(Log);
export const Pages = getCustomRepository(PageRepository);

View File

@ -6,12 +6,6 @@ import { awaitAll } from '../../prelude/await-all';
@EntityRepository(AbuseUserReport)
export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
public async pack(
src: AbuseUserReport['id'] | AbuseUserReport,
) {
@ -30,4 +24,10 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
}),
});
}
public packMany(
reports: any[],
) {
return Promise.all(reports.map(x => this.pack(x)));
}
}

View File

@ -9,13 +9,6 @@ export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
@EntityRepository(Blocking)
export class BlockingRepository extends Repository<Blocking> {
public packMany(
blockings: any[],
me: any
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
}
public async pack(
src: Blocking['id'] | Blocking,
me?: any
@ -31,6 +24,13 @@ export class BlockingRepository extends Repository<Blocking> {
})
});
}
public packMany(
blockings: any[],
me: any
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
}
}
export const packedBlockingSchema = {

View File

@ -67,17 +67,6 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0;
}
public packMany(
files: any[],
options?: {
detail?: boolean
self?: boolean,
withUser?: boolean,
}
) {
return Promise.all(files.map(f => this.pack(f, options)));
}
public async pack(
src: DriveFile['id'] | DriveFile,
options?: {
@ -111,6 +100,17 @@ export class DriveFileRepository extends Repository<DriveFile> {
user: opts.withUser ? Users.pack(file.userId!) : null
});
}
public packMany(
files: any[],
options?: {
detail?: boolean
self?: boolean,
withUser?: boolean,
}
) {
return Promise.all(files.map(f => this.pack(f, options)));
}
}
export const packedDriveFileSchema = {

View File

@ -49,17 +49,6 @@ export class FollowingRepository extends Repository<Following> {
return following.followeeHost != null;
}
public packMany(
followings: any[],
me?: any,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
}
public async pack(
src: Following['id'] | Following,
me?: any,
@ -85,6 +74,17 @@ export class FollowingRepository extends Repository<Following> {
}) : undefined,
});
}
public packMany(
followings: any[],
me?: any,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
}
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
}
}
export const packedFollowingSchema = {

View File

@ -0,0 +1,71 @@
import { EntityRepository, Repository } from 'typeorm';
import { Hashtag } from '../entities/hashtag';
import { SchemaType, types, bool } from '../../misc/schema';
export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
@EntityRepository(Hashtag)
export class HashtagRepository extends Repository<Hashtag> {
public async pack(
src: Hashtag,
): Promise<PackedHashtag> {
return {
tag: src.name,
mentionedUsersCount: src.mentionedUsersCount,
mentionedLocalUsersCount: src.mentionedLocalUsersCount,
mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
attachedUsersCount: src.attachedUsersCount,
attachedLocalUsersCount: src.attachedLocalUsersCount,
attachedRemoteUsersCount: src.attachedRemoteUsersCount,
};
}
public packMany(
hashtags: Hashtag[],
) {
return Promise.all(hashtags.map(x => this.pack(x)));
}
}
export const packedHashtagSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
properties: {
tag: {
type: types.string,
optional: bool.false, nullable: bool.false,
description: 'The hashtag name. No # prefixed.',
example: 'misskey',
},
mentionedUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of all users using this hashtag.'
},
mentionedLocalUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of local users using this hashtag.'
},
mentionedRemoteUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of remote users using this hashtag.'
},
attachedUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of all users who attached this hashtag to profile.'
},
attachedLocalUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of local users who attached this hashtag to profile.'
},
attachedRemoteUsersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
description: 'Number of remote users who attached this hashtag to profile.'
},
}
};

View File

@ -9,13 +9,6 @@ export type PackedMuting = SchemaType<typeof packedMutingSchema>;
@EntityRepository(Muting)
export class MutingRepository extends Repository<Muting> {
public packMany(
mutings: any[],
me: any
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
public async pack(
src: Muting['id'] | Muting,
me?: any
@ -31,6 +24,13 @@ export class MutingRepository extends Repository<Muting> {
})
});
}
public packMany(
mutings: any[],
me: any
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
}
}
export const packedMutingSchema = {

View File

@ -5,13 +5,6 @@ import { ensure } from '../../prelude/ensure';
@EntityRepository(NoteFavorite)
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
public packMany(
favorites: any[],
me: any
) {
return Promise.all(favorites.map(x => this.pack(x, me)));
}
public async pack(
src: NoteFavorite['id'] | NoteFavorite,
me?: any
@ -23,4 +16,11 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
note: await Notes.pack(favorite.note || favorite.noteId, me),
};
}
public packMany(
favorites: any[],
me: any
) {
return Promise.all(favorites.map(x => this.pack(x, me)));
}
}

View File

@ -76,17 +76,6 @@ export class NoteRepository extends Repository<Note> {
}
}
public packMany(
notes: (Note['id'] | Note)[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) {
return Promise.all(notes.map(n => this.pack(n, me, options)));
}
public async pack(
src: Note['id'] | Note,
me?: User['id'] | User | null | undefined,
@ -214,6 +203,17 @@ export class NoteRepository extends Repository<Note> {
return packed;
}
public packMany(
notes: (Note['id'] | Note)[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) {
return Promise.all(notes.map(n => this.pack(n, me, options)));
}
}
export const packedNoteSchema = {

View File

@ -9,12 +9,6 @@ export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
@EntityRepository(Notification)
export class NotificationRepository extends Repository<Notification> {
public packMany(
notifications: any[],
) {
return Promise.all(notifications.map(x => this.pack(x)));
}
public async pack(
src: Notification['id'] | Notification,
): Promise<PackedNotification> {
@ -48,6 +42,12 @@ export class NotificationRepository extends Repository<Notification> {
} : {})
});
}
public packMany(
notifications: any[],
) {
return Promise.all(notifications.map(x => this.pack(x)));
}
}
export const packedNotificationSchema = {

View File

@ -0,0 +1,134 @@
import { EntityRepository, Repository } from 'typeorm';
import { Page } from '../entities/page';
import { SchemaType, types, bool } from '../../misc/schema';
import { Users, DriveFiles } from '..';
import { awaitAll } from '../../prelude/await-all';
import { DriveFile } from '../entities/drive-file';
export type PackedPage = SchemaType<typeof packedPageSchema>;
@EntityRepository(Page)
export class PageRepository extends Repository<Page> {
public async pack(
src: Page,
): Promise<PackedPage> {
const attachedFiles: Promise<DriveFile | undefined>[] = [];
const collectFile = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'image') {
attachedFiles.push(DriveFiles.findOne({
id: x.fileId,
userId: src.userId
}));
}
if (x.children) {
collectFile(x.children);
}
}
};
collectFile(src.content);
// 後方互換性のため
let migrated = false;
const migrate = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'input') {
if (x.inputType === 'text') {
x.type = 'textInput';
}
if (x.inputType === 'number') {
x.type = 'numberInput';
if (x.default) x.default = parseInt(x.default, 10);
}
migrated = true;
}
if (x.children) {
migrate(x.children);
}
}
};
migrate(src.content);
if (migrated) {
this.update(src.id, {
content: src.content
});
}
return await awaitAll({
id: src.id,
createdAt: src.createdAt.toISOString(),
updatedAt: src.updatedAt.toISOString(),
userId: src.userId,
user: Users.pack(src.user || src.userId),
content: src.content,
variables: src.variables,
title: src.title,
name: src.name,
summary: src.summary,
alignCenter: src.alignCenter,
font: src.font,
eyeCatchingImageId: src.eyeCatchingImageId,
eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles))
});
}
public packMany(
pages: Page[],
) {
return Promise.all(pages.map(x => this.pack(x)));
}
}
export const packedPageSchema = {
type: types.object,
optional: bool.false, nullable: bool.false,
properties: {
id: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'date-time',
},
updatedAt: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'date-time',
},
title: {
type: types.string,
optional: bool.false, nullable: bool.false,
},
name: {
type: types.string,
optional: bool.false, nullable: bool.false,
},
summary: {
type: types.string,
optional: bool.false, nullable: bool.true,
},
content: {
type: types.array,
optional: bool.false, nullable: bool.false,
},
variables: {
type: types.array,
optional: bool.false, nullable: bool.false,
},
userId: {
type: types.string,
optional: bool.false, nullable: bool.false,
format: 'id',
},
user: {
type: types.object,
ref: 'User',
optional: bool.false, nullable: bool.false,
},
}
};

View File

@ -54,18 +54,6 @@ export class UserRepository extends Repository<User> {
};
}
public packMany(
users: (User['id'] | User)[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
) {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
public async pack(
src: User['id'] | User,
me?: User['id'] | User | null | undefined,
@ -139,6 +127,7 @@ export class UserRepository extends Repository<User> {
pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
detail: true
}),
twoFactorEnabled: profile!.twoFactorEnabled,
} : {}),
...(opts.detail && meId === user.id ? {
@ -147,7 +136,6 @@ export class UserRepository extends Repository<User> {
autoWatch: profile!.autoWatch,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
twoFactorEnabled: profile!.twoFactorEnabled,
hasUnreadMessagingMessage: MessagingMessages.count({
where: {
recipientId: user.id,
@ -187,6 +175,18 @@ export class UserRepository extends Repository<User> {
return await awaitAll(packed);
}
public packMany(
users: (User['id'] | User)[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
) {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
public isLocalUser(user: User): user is ILocalUser {
return user.host == null;
}

View File

@ -85,7 +85,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
logger.succ(`Exported to: ${path}`);
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
const driveFile = await addFile(user, path, fileName, null, null, true);
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();

View File

@ -85,7 +85,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
const driveFile = await addFile(user, path, fileName, null, null, true);
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();

View File

@ -85,7 +85,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
logger.succ(`Exported to: ${path}`);
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
const driveFile = await addFile(user, path, fileName, null, null, true);
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();

View File

@ -108,7 +108,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
logger.succ(`Exported to: ${path}`);
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
const driveFile = await addFile(user, path, fileName);
const driveFile = await addFile(user, path, fileName, null, null, true);
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();

View File

@ -62,7 +62,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
const driveFile = await addFile(user, path, fileName);
const driveFile = await addFile(user, path, fileName, null, null, true);
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();

View File

@ -35,7 +35,8 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
linenum++;
try {
const { username, host } = parseAcct(line.trim());
const acct = line.split(',')[0].trim();
const { username, host } = parseAcct(acct);
let target = isSelfHost(host!) ? await Users.findOne({
host: null,

View File

@ -14,7 +14,8 @@ type Params<T extends IEndpointMeta> = {
export type Response = Record<string, any> | void;
type executor<T extends IEndpointMeta> =
(params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
(params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
: (params: any, user: ILocalUser, app: App, file?: any) => Promise<any> {

View File

@ -0,0 +1,26 @@
import $ from 'cafy';
import define from '../define';
import endpoints from '../endpoints';
export const meta = {
requireCredential: false,
tags: ['meta'],
params: {
endpoint: {
validator: $.str,
}
},
};
export default define(meta, async (ps) => {
const ep = endpoints.find(x => x.name === ps.endpoint);
if (ep == null) return null;
return {
params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({
name: k,
type: v.validator.name === 'ID' ? 'String' : v.validator.name
}))
};
});

View File

@ -92,5 +92,5 @@ export default define(meta, async (ps, me) => {
const tags = await query.take(ps.limit!).getMany();
return tags;
return Hashtags.packMany(tags);
});

View File

@ -0,0 +1,48 @@
import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { Hashtags } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
export const meta = {
desc: {
'ja-JP': '指定したハッシュタグの情報を取得します。',
},
tags: ['hashtags'],
requireCredential: false,
params: {
tag: {
validator: $.str,
desc: {
'ja-JP': '対象のハッシュタグ(#なし)',
'en-US': 'Target hashtag. (no # prefixed)'
}
}
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
ref: 'Hashtag',
},
errors: {
noSuchHashtag: {
message: 'No such hashtag.',
code: 'NO_SUCH_HASHTAG',
id: '110ee688-193e-4a3a-9ecf-c167b2e6981e'
}
}
};
export default define(meta, async (ps, user) => {
const hashtag = await Hashtags.findOne({ name: ps.tag.toLowerCase() });
if (hashtag == null) {
throw new ApiError(meta.errors.noSuchHashtag);
}
return await Hashtags.pack(hashtag);
});

View File

@ -2,6 +2,7 @@ import define from '../../define';
import { fetchMeta } from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { types, bool } from '../../../../misc/schema';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
@ -21,6 +22,33 @@ export const meta = {
tags: ['hashtags'],
requireCredential: false,
res: {
type: types.array,
optional: bool.false, nullable: bool.false,
items: {
type: types.object,
optional: bool.false, nullable: bool.false,
properties: {
tag: {
type: types.string,
optional: bool.false, nullable: bool.false,
},
chart: {
type: types.array,
optional: bool.false, nullable: bool.false,
items: {
type: types.number,
optional: bool.false, nullable: bool.false,
}
},
usersCount: {
type: types.number,
optional: bool.false, nullable: bool.false,
}
}
}
}
};
export default define(meta, async () => {

View File

@ -0,0 +1,44 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Pages } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
desc: {
'ja-JP': '自分の作成したページ一覧を取得します。',
'en-US': 'Get my pages.'
},
tags: ['account', 'pages'],
requireCredential: true,
kind: 'read:pages',
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
}
};
export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
.andWhere(`page.userId = :meId`, { meId: user.id });
const pages = await query
.take(ps.limit!)
.getMany();
return await Pages.packMany(pages);
});

View File

@ -89,9 +89,11 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
if (user) {
activeUsersChart.update(user);
}
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
}
});
return await Notes.packMany(timeline, user);
});

View File

@ -192,9 +192,11 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
if (user) {
activeUsersChart.update(user);
}
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
}
});
return await Notes.packMany(timeline, user);
});

View File

@ -125,9 +125,11 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
if (user) {
activeUsersChart.update(user);
}
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
}
});
return await Notes.packMany(timeline, user);
});

View File

@ -177,7 +177,11 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
activeUsersChart.update(user);
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
}
});
return await Notes.packMany(timeline, user);
});

View File

@ -0,0 +1,108 @@
import $ from 'cafy';
import * as ms from 'ms';
import define from '../../define';
import { ID } from '../../../../misc/cafy-id';
import { types, bool } from '../../../../misc/schema';
import { Pages, DriveFiles } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
import { Page } from '../../../../models/entities/page';
import { ApiError } from '../../error';
export const meta = {
desc: {
'ja-JP': 'ページを作成します。',
},
tags: ['pages'],
requireCredential: true,
kind: 'write:pages',
limit: {
duration: ms('1hour'),
max: 300
},
params: {
title: {
validator: $.str,
},
name: {
validator: $.str,
},
summary: {
validator: $.optional.nullable.str,
},
content: {
validator: $.arr($.obj())
},
variables: {
validator: $.arr($.obj())
},
eyeCatchingImageId: {
validator: $.optional.nullable.type(ID),
},
font: {
validator: $.optional.str.or(['serif', 'sans-serif']),
default: 'sans-serif'
},
alignCenter: {
validator: $.optional.bool,
default: false
},
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
ref: 'Page',
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c'
},
}
};
export default define(meta, async (ps, user) => {
let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) {
eyeCatchingImage = await DriveFiles.findOne({
id: ps.eyeCatchingImageId,
userId: user.id
});
if (eyeCatchingImage == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
const page = await Pages.save(new Page({
id: genId(),
createdAt: new Date(),
updatedAt: new Date(),
title: ps.title,
name: ps.name,
summary: ps.summary,
content: ps.content,
variables: ps.variables,
eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null,
userId: user.id,
visibility: 'public',
alignCenter: ps.alignCenter,
font: ps.font
}));
return await Pages.pack(page);
});

View File

@ -0,0 +1,53 @@
import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { Pages } from '../../../../models';
import { ID } from '../../../../misc/cafy-id';
export const meta = {
desc: {
'ja-JP': '指定したページを削除します。',
},
tags: ['pages'],
requireCredential: true,
kind: 'write:pages',
params: {
pageId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のページのID',
'en-US': 'Target page ID.'
}
},
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a'
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '8b741b3e-2c22-44b3-a15f-29949aa1601e'
},
}
};
export default define(meta, async (ps, user) => {
const page = await Pages.findOne(ps.pageId);
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId !== user.id) {
throw new ApiError(meta.errors.accessDenied);
}
await Pages.delete(page.id);
});

View File

@ -0,0 +1,74 @@
import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { Pages, Users } from '../../../../models';
import { types, bool } from '../../../../misc/schema';
import { ID } from '../../../../misc/cafy-id';
import { Page } from '../../../../models/entities/page';
export const meta = {
desc: {
'ja-JP': '指定したページの情報を取得します。',
},
tags: ['pages'],
requireCredential: false,
params: {
pageId: {
validator: $.optional.type(ID),
desc: {
'ja-JP': '対象のページのID',
'en-US': 'Target page ID.'
}
},
name: {
validator: $.optional.str,
},
username: {
validator: $.optional.str,
},
},
res: {
type: types.object,
optional: bool.false, nullable: bool.false,
ref: 'Page',
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: '222120c0-3ead-4528-811b-b96f233388d7'
}
}
};
export default define(meta, async (ps, user) => {
let page: Page | undefined;
if (ps.pageId) {
page = await Pages.findOne(ps.pageId);
} else if (ps.name && ps.username) {
const author = await Users.findOne({
host: null,
usernameLower: ps.username.toLowerCase()
});
if (author) {
page = await Pages.findOne({
name: ps.name,
userId: author.id
});
}
}
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
return await Pages.pack(page);
});

View File

@ -0,0 +1,123 @@
import $ from 'cafy';
import * as ms from 'ms';
import define from '../../define';
import { ApiError } from '../../error';
import { Pages, DriveFiles } from '../../../../models';
import { ID } from '../../../../misc/cafy-id';
export const meta = {
desc: {
'ja-JP': '指定したページの情報を更新します。',
},
tags: ['pages'],
requireCredential: true,
kind: 'write:pages',
limit: {
duration: ms('1hour'),
max: 300
},
params: {
pageId: {
validator: $.type(ID),
desc: {
'ja-JP': '対象のページのID',
'en-US': 'Target page ID.'
}
},
title: {
validator: $.str,
},
name: {
validator: $.optional.str,
},
summary: {
validator: $.optional.nullable.str,
},
content: {
validator: $.arr($.obj())
},
variables: {
validator: $.arr($.obj())
},
eyeCatchingImageId: {
validator: $.optional.nullable.type(ID),
},
font: {
validator: $.optional.str.or(['serif', 'sans-serif']),
},
alignCenter: {
validator: $.optional.bool,
},
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864'
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b'
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'cfc23c7c-3887-490e-af30-0ed576703c82'
},
}
};
export default define(meta, async (ps, user) => {
const page = await Pages.findOne(ps.pageId);
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId !== user.id) {
throw new ApiError(meta.errors.accessDenied);
}
let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) {
eyeCatchingImage = await DriveFiles.findOne({
id: ps.eyeCatchingImageId,
userId: user.id
});
if (eyeCatchingImage == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
await Pages.update(page.id, {
updatedAt: new Date(),
title: ps.title,
name: ps.name === undefined ? page.name : ps.name,
summary: ps.name === undefined ? page.summary : ps.summary,
content: ps.content,
variables: ps.variables,
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
font: ps.font === undefined ? page.font : ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId === null
? null
: ps.eyeCatchingImageId === undefined
? page.eyeCatchingImageId
: eyeCatchingImage!.id,
});
});

Some files were not shown because too many files have changed in this diff Show More