diff --git a/.config/example.yml b/.config/example.yml
index 48b1a0fd1c..2591066da5 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -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'
# ┌─────────────────────┐
diff --git a/.gitignore b/.gitignore
index 650d4f6128..255b1ad4d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ api-docs.json
yarn.lock
.DS_Store
/files
+ormconfig.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f50bcef576..a2ed4e4437 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ If you encounter any problems with updating, please try the following:
How to migrate to v11 from v10
------------------------------
+### 移行の注意点
+**以下のデータは引き継がれません**
+* 通知
+* リモートの投稿
+* リバーシの対局
+
+### 手順
1. v11をインストールしたい場所に syuilo/misskey をクローン
2. config を設定する
* PostgreSQL(`db`)の設定とは別に、v10からMongoDBの設定をコピペしてくる(例は下にあります)
@@ -35,6 +42,64 @@ mongodb:
8. master ブランチに戻す
9. enjoy
+11.5.0 (2019/04/29)
+-------------------
+### 注意
+このアップデートを適用した後、プロセスを起動(もしくは再起動)する前にまず以下の手順を実行してください
+
+#### 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
+```
+
+### New features
+#### MisskeyPages
+ページ(記事)を作成できるように。
+
+* 後から何度でも編集できる
+* アイキャッチを設定できる
+* フォントを設定できる
+* 画像を好きな位置に挿入できる
+* URLを決められる
+* タイトルを設定できる
+* 見出しを設定できる
+* ページの要約を設定できる(URLプレビュー時などに便利)
+* 変数や式(aka AiScript)を使用して動的なページも作れる
+* 目次自動生成(coming soon)
+
+ページを気に入ったら「いいね」しよう (coming soon)
+
+### Improvements
+* APIコンソールでパラメータテンプレートを表示するように
+
+### Fixes
+* おすすめユーザーに自分自身が含まれる問題を修正
+* ユーザーサジェストで表示名が変わらない問題を修正
+
11.4.0 (2019/04/25)
-------------------
### Improvements
diff --git a/README.md b/README.md
index f69c503a90..1fae71df4e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-[![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.**
-Misskey is a decentralized microblogging platform born on Earth.
+Misskey 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? Find an instance!
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a475bc2c16..b0cb78f96b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -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,165 @@ dev/views/new-app.vue:
authority: "権限"
authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
+
+pages:
+ new-page: "ページの作成"
+ edit-page: "ページの編集"
+ page-created: "ページを作成しました"
+ page-updated: "ページを更新しました"
+ are-you-sure-delete: "このページを削除しますか?"
+ page-deleted: "ページを削除しました"
+ edit-this-page: "このページを編集"
+ variables: "変数"
+ variables-info: "変数を使うことで動的なページを作成できます。テキスト内で { 変数名 } と書くとそこに変数の値を埋め込めます。例えば Hello { thing } world! というテキストで、変数(thing)の値が ai だった場合、テキストは Hello ai world! になります。"
+ variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から A、B、C と3つの変数を定義したとき、Cの中でAやBを参照することはできますが、Aの中でBやCを参照することはできません。"
+ variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。"
+ 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: "その変数名は既に使われています"
+ blocks:
+ text: "テキスト"
+ section: "セクション"
+ image: "画像"
+ button: "ボタン"
+ input: "ユーザー入力"
+ _input:
+ name: "変数名"
+ text: "タイトル"
+ default: "デフォルト値"
+ inputType: "入力の種類"
+ _inputType:
+ string: "テキスト"
+ number: "数値"
+ switch: "スイッチ"
+ _switch:
+ name: "変数名"
+ text: "タイトル"
+ default: "デフォルト値"
+ _button:
+ text: "タイトル"
+ action: "ボタンを押したときの動作"
+ _action:
+ dialog: "ダイアログを表示する"
+ _dialog:
+ content: "内容"
+ resetRandom: "乱数をリセット"
+ script:
+ categories:
+ flow: "制御"
+ logical: "論理演算"
+ operation: "計算"
+ comparison: "比較"
+ random: "ランダム"
+ value: "値"
+ fn: "関数"
+ blocks:
+ text: "テキスト"
+ multiLineText: "テキスト(複数行)"
+ textList: "テキストのリスト"
+ add: "+ 足す"
+ _add:
+ arg1: "A"
+ arg2: "B"
+ subtract: "- 引く"
+ _subtract:
+ arg1: "A"
+ arg2: "B"
+ multiply: "× 掛ける"
+ _multiply:
+ arg1: "A"
+ arg2: "B"
+ divide: "÷ 割る"
+ _divide:
+ 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: "リスト"
+ number: "数"
+ ref: "変数"
+ in: "入力"
+ _in:
+ arg1: "スロット番号"
+ fn: "関数"
+ _fn:
+ arg1: "出力"
+ typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
+ thereIsEmptySlot: "スロット{slot}が空です!"
+ types:
+ string: "テキスト"
+ number: "数値"
+ boolean: "フラグ"
+ array: "リスト"
+ stringArray: "テキストのリスト"
+ emptySlot: "空のスロット"
+ enviromentVariables: "環境変数"
+ pageVariables: "ページ要素"
diff --git a/migration/1556348509290-Pages.ts b/migration/1556348509290-Pages.ts
new file mode 100644
index 0000000000..c44b4b1f79
--- /dev/null
+++ b/migration/1556348509290-Pages.ts
@@ -0,0 +1,31 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class Pages1556348509290 implements MigrationInterface {
+
+ public async up(queryRunner: QueryRunner): Promise {
+ 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 {
+ 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"`);
+ }
+
+}
diff --git a/package.json b/package.json
index b1900c8fa7..14d3bcc8ff 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo ",
- "version": "11.4.0",
+ "version": "11.5.0",
"codename": "daybreak",
"repository": {
"type": "git",
@@ -93,13 +93,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 +120,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 +139,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 +159,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 +199,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 +209,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 +233,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",
diff --git a/src/client/app/common/scripts/aiscript.ts b/src/client/app/common/scripts/aiscript.ts
new file mode 100644
index 0000000000..4ef21f9943
--- /dev/null
+++ b/src/client/app/common/scripts/aiscript.ts
@@ -0,0 +1,470 @@
+/**
+ * AiScript
+ * evaluator & type checker
+ */
+
+import autobind from 'autobind-decorator';
+import * as seedrandom from 'seedrandom';
+
+import {
+ faSuperscript,
+ faAlignLeft,
+ faShareAlt,
+ faSquareRootAlt,
+ faPlus,
+ faMinus,
+ faTimes,
+ faDivide,
+ faList,
+ faQuoteRight,
+ faEquals,
+ faGreaterThan,
+ faLessThan,
+ faGreaterThanEqual,
+ faLessThanEqual,
+ faExclamation,
+ faNotEqual,
+ faDice,
+ faSortNumericUp,
+} from '@fortawesome/free-solid-svg-icons';
+import { faFlag } from '@fortawesome/free-regular-svg-icons';
+
+import { version } from '../../config';
+
+export type Block = {
+ id: string;
+ type: string;
+ args: Block[];
+ value: any;
+};
+
+export type Variable = Block & {
+ name: string;
+};
+
+type Type = 'string' | 'number' | 'boolean' | 'stringArray';
+
+type TypeError = {
+ arg: number;
+ expect: Type;
+ actual: Type;
+};
+
+const funcDefs = {
+ if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, },
+ 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, },
+ 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, },
+ rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
+ random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
+ randomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
+ dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
+ dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
+ dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
+};
+
+const blockDefs = [
+ { type: 'text', out: 'string', category: 'value', icon: faQuoteRight, },
+ { type: 'multiLineText', out: 'string', category: 'value', icon: faAlignLeft, },
+ { type: 'textList', out: 'stringArray', category: 'value', icon: faList, },
+ { type: 'number', out: 'number', category: 'value', icon: faSortNumericUp, },
+ { type: 'ref', out: null, category: 'value', icon: faSuperscript, },
+ { type: 'in', out: null, category: 'value', icon: faSuperscript, },
+ { type: 'fn', out: 'function', category: 'value', icon: faSuperscript, },
+ ...Object.entries(funcDefs).map(([k, v]) => ({
+ type: k, out: v.out || null, category: v.category, icon: v.icon
+ }))
+];
+
+type PageVar = { name: string; value: any; type: Type; };
+
+const envVarsDef = {
+ AI: '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',
+};
+
+export class AiScript {
+ private variables: Variable[];
+ private pageVars: PageVar[];
+ private envVars: Record;
+
+ public static envVarsDef = envVarsDef;
+ public static blockDefs = blockDefs;
+ public static funcDefs = funcDefs;
+ private opts: {
+ randomSeed?: string; user?: any; visitor?: any;
+ };
+
+ constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) {
+ this.variables = variables;
+ this.pageVars = pageVars;
+ this.opts = opts;
+
+ this.envVars = {
+ AI: 'kawaii',
+ VERSION: version,
+ 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,
+ };
+ }
+
+ @autobind
+ public injectVars(vars: Variable[]) {
+ this.variables = vars;
+ }
+
+ @autobind
+ public injectPageVars(pageVars: PageVar[]) {
+ this.pageVars = pageVars;
+ }
+
+ @autobind
+ public updatePageVar(name: string, value: any) {
+ this.pageVars.find(v => v.name === name).value = value;
+ }
+
+ @autobind
+ public updateRandomSeed(seed: string) {
+ this.opts.randomSeed = seed;
+ }
+
+ @autobind
+ public static isLiteralBlock(v: Block) {
+ if (v.type === null) return true;
+ if (v.type === 'text') return true;
+ if (v.type === 'multiLineText') return true;
+ if (v.type === 'textList') return true;
+ if (v.type === 'number') return true;
+ if (v.type === 'ref') return true;
+ if (v.type === 'fn') return true;
+ if (v.type === 'in') return true;
+ return false;
+ }
+
+ @autobind
+ public typeCheck(v: Block): TypeError | null {
+ if (AiScript.isLiteralBlock(v)) return null;
+
+ const def = AiScript.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.typeInference(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 | null {
+ const def = AiScript.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.typeInference(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 typeInference(v: Block): Type | null {
+ 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.typeInference(variable);
+ }
+
+ const pageVar = this.pageVars.find(va => va.name === v.value);
+ if (pageVar) {
+ return pageVar.type;
+ }
+
+ const envVar = AiScript.envVarsDef[v.value];
+ if (envVar) {
+ return envVar;
+ }
+
+ return null;
+ }
+ if (v.type === 'fn') return null; // todo
+ if (v.type === 'in') return null; // todo
+
+ const generic: Type[] = [];
+
+ const def = AiScript.funcDefs[v.type];
+
+ for (let i = 0; i < def.in.length; i++) {
+ const arg = def.in[i];
+ if (typeof arg === 'number') {
+ const type = this.typeInference(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 getVarsByType(type: Type | null): Variable[] {
+ if (type == null) return this.variables;
+ return this.variables.filter(x => (this.typeInference(x) === null) || (this.typeInference(x) === type));
+ }
+
+ @autobind
+ public getVarByName(name: string): Variable {
+ return this.variables.find(x => x.name === name);
+ }
+
+ @autobind
+ public getEnvVarsByType(type: Type | null): string[] {
+ if (type == null) return Object.keys(AiScript.envVarsDef);
+ return Object.entries(AiScript.envVarsDef).filter(([k, v]) => type === v).map(([k, v]) => k);
+ }
+
+ @autobind
+ public getPageVarsByType(type: Type | null): string[] {
+ if (type == null) return this.pageVars.map(v => v.name);
+ return this.pageVars.filter(v => type === v.type).map(v => v.name);
+ }
+
+ @autobind
+ private interpolate(str: string, values: { name: string, value: any }[]) {
+ return str.replace(/\{(.+?)\}/g, match =>
+ (this.getVariableValue(match.slice(1, -1).trim(), values) || '').toString());
+ }
+
+ @autobind
+ public evaluateVars() {
+ const values: { name: string, value: any }[] = [];
+
+ for (const v of this.variables) {
+ values.push({
+ name: v.name,
+ value: this.evaluate(v, values)
+ });
+ }
+
+ for (const v of this.pageVars) {
+ values.push({
+ name: v.name,
+ value: v.value
+ });
+ }
+
+ for (const [k, v] of Object.entries(this.envVars)) {
+ values.push({
+ name: k,
+ value: v
+ });
+ }
+
+ return values;
+ }
+
+ @autobind
+ private evaluate(block: Block, values: { name: string, value: any }[], slotArg: Record = {}): 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, values);
+ }
+
+ if (block.type === 'textList') {
+ return block.value.trim().split('\n');
+ }
+
+ if (block.type === 'ref') {
+ return this.getVariableValue(block.value, values);
+ }
+
+ if (block.type === 'in') {
+ return slotArg[block.value];
+ }
+
+ if (block.type === 'fn') { // ユーザー関数定義
+ return {
+ slots: block.value.slots,
+ exec: slotArg => this.evaluate(block.value.expression, values, slotArg)
+ };
+ }
+
+ if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
+ const fnName = block.type.split(':')[1];
+ const fn = this.getVariableValue(fnName, values);
+ for (let i = 0; i < fn.slots.length; i++) {
+ const name = fn.slots[i];
+ slotArg[name] = this.evaluate(block.args[i], values);
+ }
+ return fn.exec(slotArg);
+ }
+
+ if (block.args === undefined) return null;
+
+ const date = new Date();
+ const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth()}/${date.getDate()}`;
+
+ const funcs: { [p in keyof typeof funcDefs]: any } = {
+ not: (a) => !a,
+ eq: (a, b) => a === b,
+ notEq: (a, b) => a !== b,
+ gt: (a, b) => a > b,
+ lt: (a, b) => a < b,
+ gtEq: (a, b) => a >= b,
+ ltEq: (a, b) => a <= b,
+ or: (a, b) => a || b,
+ and: (a, b) => a && b,
+ if: (bool, a, b) => bool ? a : b,
+ add: (a, b) => a + b,
+ subtract: (a, b) => a - b,
+ multiply: (a, b) => a * b,
+ divide: (a, b) => a / b,
+ random: (probability) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability,
+ rannum: (min, max) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)),
+ randomPick: (list) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)],
+ dailyRandom: (probability) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
+ dailyRannum: (min, max) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
+ dailyRandomPick: (list) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
+ };
+
+ const fnName = block.type;
+
+ const fn = funcs[fnName];
+ if (fn == null) {
+ console.error('Unknown function: ' + fnName);
+ throw new Error('Unknown function: ' + fnName);
+ }
+
+ const args = block.args.map(x => this.evaluate(x, values, slotArg));
+
+ return fn(...args);
+ }
+
+ @autobind
+ private getVariableValue(name: string, values: { name: string, value: any }[]): any {
+ const v = values.find(v => v.name === name);
+ if (v) {
+ return v.value;
+ }
+
+ const pageVar = this.pageVars.find(v => v.name === name);
+ if (pageVar) {
+ return pageVar.value;
+ }
+
+ if (AiScript.envVarsDef[name]) {
+ return this.envVars[name].value;
+ }
+
+ throw new Error(`Script: No such variable '${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 (AiScript.envVarsDef[name]) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/app/common/scripts/collect-page-vars.ts
new file mode 100644
index 0000000000..86687e21f4
--- /dev/null
+++ b/src/client/app/common/scripts/collect-page-vars.ts
@@ -0,0 +1,24 @@
+export function collectPageVars(content) {
+ const pageVars = [];
+ const collect = (xs: any[]) => {
+ for (const x of xs) {
+ if (x.type === 'input') {
+ pageVars.push({
+ name: x.name,
+ type: x.inputType,
+ value: x.default
+ });
+ } else if (x.type === 'switch') {
+ pageVars.push({
+ name: x.name,
+ type: 'boolean',
+ value: x.default
+ });
+ } else if (x.children) {
+ collect(x.children);
+ }
+ }
+ };
+ collect(content);
+ return pageVars;
+}
diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue
index c1ee7958c0..020c88f699 100644
--- a/src/client/app/common/views/components/dialog.vue
+++ b/src/client/app/common/views/components/dialog.vue
@@ -22,7 +22,14 @@
@
-
+
+
+
+
+
+
{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}
@@ -230,7 +237,7 @@ export default Vue.extend({
font-size 32px
&.success
- color #37ec92
+ color #85da5a
&.error
color #ec4137
diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue
index 2559907512..6db4b40dd8 100644
--- a/src/client/app/common/views/components/media-image.vue
+++ b/src/client/app/common/views/components/media-image.vue
@@ -36,7 +36,7 @@ export default Vue.extend({
return {
hide: true
};
- }
+ },
computed: {
style(): any {
let url = `url(${
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index ad3b639a28..957fd389d9 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -13,8 +13,8 @@
@click="navigate(user)"
tabindex="-1"
>
-
-
+
+
@{{ user | acct }}
diff --git a/src/client/app/common/views/components/page-editor/page-editor.block.vue b/src/client/app/common/views/components/page-editor/page-editor.block.vue
new file mode 100644
index 0000000000..a3e1488d1b
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.block.vue
@@ -0,0 +1,25 @@
+
+ updateItem(v)" @remove="() => $emit('remove', value)" :key="value.id"/>
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.button.vue b/src/client/app/common/views/components/page-editor/page-editor.button.vue
new file mode 100644
index 0000000000..d5fc243818
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.button.vue
@@ -0,0 +1,54 @@
+
+ $emit('remove')">
+ {{ $t('blocks.button') }}
+
+
+ {{ $t('blocks._button.text') }}
+
+ {{ $t('blocks._button.action') }}
+
+
+
+ {{ $t('blocks._button._action._dialog.content') }}
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.container.vue b/src/client/app/common/views/components/page-editor/page-editor.container.vue
new file mode 100644
index 0000000000..698fdfee45
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.container.vue
@@ -0,0 +1,135 @@
+
+
+
+
{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}
+
{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}
+
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.image.vue b/src/client/app/common/views/components/page-editor/page-editor.image.vue
new file mode 100644
index 0000000000..0bc1816e8d
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.image.vue
@@ -0,0 +1,78 @@
+
+ $emit('remove')">
+ {{ $t('blocks.image') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.input.vue b/src/client/app/common/views/components/page-editor/page-editor.input.vue
new file mode 100644
index 0000000000..1f3754252b
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.input.vue
@@ -0,0 +1,54 @@
+
+ $emit('remove')">
+ {{ $t('blocks.input') }}
+
+
+ {{ $t('blocks._input.name') }}
+ {{ $t('blocks._input.text') }}
+
+ {{ $t('blocks._input.inputType') }}
+
+
+
+ {{ $t('blocks._input.default') }}
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue
new file mode 100644
index 0000000000..3122832030
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue
@@ -0,0 +1,263 @@
+
+ $emit('remove')" :error="error" :warn="warn">
+ {{ title }} ({{ typeText }}){{ typeText }}
+
+
+
+
+
+ {{ $t('script.emptySlot') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.section.vue b/src/client/app/common/views/components/page-editor/page-editor.section.vue
new file mode 100644
index 0000000000..d7a247b0b1
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.section.vue
@@ -0,0 +1,133 @@
+
+ $emit('remove')">
+ {{ value.title }}
+
+
+
+
+
+
+
+ updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.switch.vue b/src/client/app/common/views/components/page-editor/page-editor.switch.vue
new file mode 100644
index 0000000000..a9cfa2844f
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.switch.vue
@@ -0,0 +1,48 @@
+
+ $emit('remove')">
+ {{ $t('blocks.switch') }}
+
+
+ {{ $t('blocks._switch.name') }}
+ {{ $t('blocks._switch.text') }}
+ {{ $t('blocks._switch.default') }}
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.text.vue b/src/client/app/common/views/components/page-editor/page-editor.text.vue
new file mode 100644
index 0000000000..7368931b2f
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.text.vue
@@ -0,0 +1,57 @@
+
+ $emit('remove')">
+ {{ $t('blocks.text') }}
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-editor/page-editor.vue b/src/client/app/common/views/components/page-editor/page-editor.vue
new file mode 100644
index 0000000000..1bcaaa0330
--- /dev/null
+++ b/src/client/app/common/views/components/page-editor/page-editor.vue
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+ {{ $t('title') }}
+
+
+
+
+ {{ $t('summary') }}
+
+
+
+ {{ url }}/@{{ $store.state.i.username }}/pages/
+ {{ $t('url') }}
+
+
+ {{ $t('align-center') }}
+
+
+ {{ $t('font') }}
+
+
+
+
+
+
{{ $t('set-eye-catchig-image') }}
+
+
+
{{ $t('remove-eye-catchig-image') }}
+
+
+
+
+
+ updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
+
+
+
+
+
+
+
+ {{ $t('variables') }}
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue
new file mode 100644
index 0000000000..d8fdbf4b04
--- /dev/null
+++ b/src/client/app/common/views/components/page-preview.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+ {{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}
+
+
+
+
+
+
+
+
diff --git a/src/client/app/common/views/components/settings/api.vue b/src/client/app/common/views/components/settings/api.vue
index 74e3eb0661..184fa069fb 100644
--- a/src/client/app/common/views/components/settings/api.vue
+++ b/src/client/app/common/views/components/settings/api.vue
@@ -14,7 +14,7 @@
{{ $t('console.title') }}
-
+
{{ $t('console.endpoint') }}
@@ -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);
+ });
}
}
});
diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue
index bcb87398ba..645062df28 100644
--- a/src/client/app/common/views/components/ui/input.vue
+++ b/src/client/app/common/views/components/ui/input.vue
@@ -23,6 +23,7 @@
@focus="focused = true"
@blur="focused = false"
@keydown="$emit('keydown', $event)"
+ @change="$emit('change', $event)"
:list="id"
>