Merge branch 'develop' into sw-notification-action

This commit is contained in:
tamaina
2021-02-09 20:14:05 +09:00
126 changed files with 2668 additions and 1636 deletions

View File

@@ -1,16 +0,0 @@
declare module 'recaptcha-promise' {
interface IVerifyOptions {
secret_key?: string;
}
interface IVerify {
(response: string, remoteAddress?: string): Promise<boolean>;
init(options: IVerifyOptions): IVerify;
}
namespace recaptchaPromise {} // Hack
const verify: IVerify;
export = verify;
}

View File

@@ -57,7 +57,7 @@ export default defineComponent({
src() {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
grecaptcha: 'https://www.google.com/recaptcha',
grecaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<PropertyKey, unknown>)[this.provider];
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;

View File

@@ -21,13 +21,13 @@
>
<div class="contents" ref="contents">
<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
<XFolder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
<XFolder v-for="(f, i) in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" v-anim="i"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<MkButton ref="moreFolders" v-if="moreFolders">{{ $ts.loadMore }}</MkButton>
</div>
<div class="files" ref="filesContainer" v-show="files.length > 0">
<XFile v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
<XFile v-for="(file, i) in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" v-anim="i"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="(n, i) in 16" :key="i"></div>
<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $ts.loadMore }}</MkButton>

View File

@@ -2,6 +2,7 @@
<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>

View File

@@ -52,7 +52,7 @@
<span class="localOnly" v-if="appearNote.localOnly"><Fa :icon="faBiohazard"/></span>
</div>
<div class="username"><MkAcct :user="appearNote.user"/></div>
<MkInstanceTicker class="ticker" :instance="appearNote.user.instance"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
</div>
</header>
<div class="main">

View File

@@ -1,9 +1,9 @@
<template>
<component :is="'x-' + value.type" :value="value" :page="page" :hpml="hpml" :key="value.id" :h="h"/>
<component :is="'x-' + block.type" :block="block" :hpml="hpml" :key="block.id" :h="h"/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
@@ -19,22 +19,24 @@ import XCounter from './page.counter.vue';
import XRadioButton from './page.radio-button.vue';
import XCanvas from './page.canvas.vue';
import XNote from './page.note.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { Block } from '@/scripts/hpml/block';
export default defineComponent({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote
},
props: {
value: {
block: {
type: Object as PropType<Block>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {
type: Number,
required: true
}
},

View File

@@ -1,51 +1,55 @@
<template>
<div>
<MkButton class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</MkButton>
<MkButton class="kudkigyw" @click="click()" :primary="block.primary">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType, unref } from 'vue';
import MkButton from '../ui/button.vue';
import * as os from '@/os';
import { ButtonBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton
},
props: {
value: {
block: {
type: Object as PropType<ButtonBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
methods: {
click() {
if (this.value.action === 'dialog') {
if (this.block.action === 'dialog') {
this.hpml.eval();
os.dialog({
text: this.hpml.interpolate(this.value.content)
text: this.hpml.interpolate(this.block.content)
});
} else if (this.value.action === 'resetRandom') {
} else if (this.block.action === 'resetRandom') {
this.hpml.updateRandomSeed(Math.random());
this.hpml.eval();
} else if (this.value.action === 'pushEvent') {
} else if (this.block.action === 'pushEvent') {
os.api('page-push', {
pageId: this.hpml.page.id,
event: this.value.event,
...(this.value.var ? {
var: this.hpml.vars[this.value.var]
event: this.block.event,
...(this.block.var ? {
var: unref(this.hpml.vars)[this.block.var]
} : {})
});
os.dialog({
type: 'success',
text: this.hpml.interpolate(this.value.message)
text: this.hpml.interpolate(this.block.message)
});
} else if (this.value.action === 'callAiScript') {
this.hpml.callAiScript(this.value.fn);
} else if (this.block.action === 'callAiScript') {
this.hpml.callAiScript(this.block.fn);
}
}
}

View File

@@ -1,24 +1,36 @@
<template>
<div class="ysrxegms">
<canvas ref="canvas" :width="value.width" :height="value.height"/>
<canvas ref="canvas" :width="block.width" :height="block.height"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
import * as os from '@/os';
import { CanvasBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
value: {
block: {
type: Object as PropType<CanvasBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
mounted() {
this.hpml.registerCanvas(this.value.name, this.$refs.canvas);
setup(props, ctx) {
const canvas: Ref<any> = ref(null);
onMounted(() => {
props.hpml.registerCanvas(props.block.name, canvas.value);
});
return {
canvas
};
}
});
</script>

View File

@@ -1,41 +1,43 @@
<template>
<div>
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</MkButton>
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkButton from '../ui/button.vue';
import * as os from '@/os';
import { CounterVarBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton
},
props: {
value: {
block: {
type: Object as PropType<CounterVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function click() {
props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
props.hpml.eval();
}
return {
v: 0,
click
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
}
},
methods: {
click() {
this.v = this.v + (this.value.inc || 1);
}
}
});
</script>

View File

@@ -1,27 +1,29 @@
<template>
<div v-show="hpml.vars[value.var]">
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/>
<div v-show="hpml.vars.value[block.var]">
<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h"/>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { IfBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue'))
},
props: {
value: {
block: {
type: Object as PropType<IfBlock>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {
type: Number,
required: true
}
},

View File

@@ -5,25 +5,28 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import * as os from '@/os';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
value: {
block: {
type: Object as PropType<ImageBlock>,
required: true
},
page: {
hpml: {
type: Object as PropType<Hpml>,
required: true
},
}
},
data() {
setup(props, ctx) {
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
return {
image: null,
image
};
},
created() {
this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
}
});
</script>

View File

@@ -1,15 +1,16 @@
<template>
<div class="voxdxuby">
<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'"/>
<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'"/>
<XNote v-if="note && !block.detailed" v-model:note="note" :key="note.id + ':normal'"/>
<XNoteDetailed v-if="note && block.detailed" v-model:note="note" :key="note.id + ':detail'"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
import XNote from '@/components/note.vue';
import XNoteDetailed from '@/components/note-detailed.vue';
import * as os from '@/os';
import { NoteBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
@@ -17,20 +18,24 @@ export default defineComponent({
XNoteDetailed,
},
props: {
value: {
required: true
},
hpml: {
block: {
type: Object as PropType<NoteBlock>,
required: true
}
},
data() {
setup(props, ctx) {
const note: Ref<Record<string, any> | null> = ref(null);
onMounted(() => {
os.api('notes/show', { noteId: props.block.note })
.then(result => {
note.value = result;
});
});
return {
note: null,
note
};
},
async mounted() {
this.note = await os.api('notes/show', { noteId: this.value.note });
}
});
</script>

View File

@@ -1,36 +1,44 @@
<template>
<div>
<MkInput class="kudkigyw" v-model:value="v" type="number">{{ hpml.interpolate(value.text) }}</MkInput>
<MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="number">{{ hpml.interpolate(block.text) }}</MkInput>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput
},
props: {
value: {
block: {
type: Object as PropType<NumberInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@@ -6,12 +6,14 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
import MkTextarea from '../ui/textarea.vue';
import MkButton from '../ui/button.vue';
import { apiUrl } from '@/config';
import * as os from '@/os';
import { PostBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
@@ -19,16 +21,18 @@ export default defineComponent({
MkButton,
},
props: {
value: {
block: {
type: Object as PropType<PostBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
posted: false,
posting: false,
faCheck, faPaperPlane
@@ -37,7 +41,7 @@ export default defineComponent({
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}
@@ -45,7 +49,7 @@ export default defineComponent({
methods: {
upload() {
const promise = new Promise((ok) => {
const canvas = this.hpml.canvases[this.value.canvasId];
const canvas = this.hpml.canvases[this.block.canvasId];
canvas.toBlob(blob => {
const data = new FormData();
data.append('file', blob);
@@ -69,7 +73,7 @@ export default defineComponent({
},
async post() {
this.posting = true;
const file = this.value.attachCanvasImage ? await this.upload() : null;
const file = this.block.attachCanvasImage ? await this.upload() : null;
os.apiWithDialog('notes/create', {
text: this.text === '' ? null : this.text,
fileIds: file ? [file.id] : undefined,

View File

@@ -1,37 +1,45 @@
<template>
<div>
<div>{{ hpml.interpolate(value.title) }}</div>
<MkRadio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</MkRadio>
<div>{{ hpml.interpolate(block.title) }}</div>
<MkRadio v-for="item in block.values" :modelValue="value" @update:modelValue="updateValue($event)" :value="item" :key="item">{{ item }}</MkRadio>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkRadio from '../ui/radio.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkRadio
},
props: {
value: {
block: {
type: Object as PropType<RadioButtonVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: string) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@@ -1,29 +1,30 @@
<template>
<section class="sdgxphyu">
<component :is="'h' + h">{{ value.title }}</component>
<component :is="'h' + h">{{ block.title }}</component>
<div class="children">
<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/>
<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h + 1"/>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
import * as os from '@/os';
import { SectionBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue'))
},
props: {
value: {
block: {
type: Object as PropType<SectionBlock>,
required: true
},
hpml: {
required: true
},
page: {
type: Object as PropType<Hpml>,
required: true
},
h: {

View File

@@ -1,36 +1,44 @@
<template>
<div class="hkcxmtwj">
<MkSwitch v-model:value="v">{{ hpml.interpolate(value.text) }}</MkSwitch>
<MkSwitch :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkSwitch from '../ui/switch.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkSwitch
},
props: {
value: {
block: {
type: Object as PropType<SwitchVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: boolean) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@@ -1,36 +1,44 @@
<template>
<div>
<MkInput class="kudkigyw" v-model:value="v" type="text">{{ hpml.interpolate(value.text) }}</MkInput>
<MkInput class="kudkigyw" :value="value" @update:value="updateValue($event)" type="text">{{ hpml.interpolate(block.text) }}</MkInput>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../ui/input.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput
},
props: {
value: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@@ -6,7 +6,9 @@
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineAsyncComponent, defineComponent, PropType } from 'vue';
import { parse } from '../../../mfm/parse';
import { unique } from '../../../prelude/array';
@@ -15,16 +17,18 @@ export default defineComponent({
MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
},
props: {
value: {
block: {
type: Object as PropType<TextBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
};
},
computed: {
@@ -43,7 +47,7 @@ export default defineComponent({
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}

View File

@@ -1,36 +1,45 @@
<template>
<div>
<MkTextarea v-model:value="v">{{ hpml.interpolate(value.text) }}</MkTextarea>
<MkTextarea :value="value" @update:value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkTextarea>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue';
import * as os from '@/os';
import { Hpml } from '@/scripts/hpml/evaluator';
import { HpmlTextInput } from '@/scripts/hpml';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkTextarea
},
props: {
value: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.hpml.updatePageVar(this.value.name, this.v);
this.hpml.eval();
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue
};
}
});
</script>

View File

@@ -3,7 +3,9 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue';
import MkTextarea from '../ui/textarea.vue';
export default defineComponent({
@@ -11,22 +13,24 @@ export default defineComponent({
MkTextarea
},
props: {
value: {
block: {
type: Object as PropType<TextBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
data() {
return {
text: this.hpml.interpolate(this.value.text),
text: this.hpml.interpolate(this.block.text),
};
},
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.value.text);
this.text = this.hpml.interpolate(this.block.text);
},
deep: true
}

View File

@@ -1,77 +1,72 @@
<template>
<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml">
<XBlock v-for="child in page.content" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="2"/>
<XBlock v-for="child in page.content" :block="child" :hpml="hpml" :key="child.id" :h="2"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, nextTick, onUnmounted, PropType } from 'vue';
import { parse } from '@syuilo/aiscript';
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
import { faHeart } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { url } from '@/config';
import { $i } from '@/account';
import { defaultStore } from '@/store';
export default defineComponent({
components: {
XBlock
},
props: {
page: {
type: Object,
type: Object as PropType<Record<string, any>>,
required: true
},
},
setup(props, ctx) {
data() {
return {
hpml: null,
faHeartS, faHeart
};
},
created() {
this.hpml = new Hpml(this.page, {
const hpml = new Hpml(props.page, {
randomSeed: Math.random(),
visitor: this.$i,
visitor: $i,
url: url,
enableAiScript: !this.$store.state.disablePagesScript
enableAiScript: !defaultStore.state.disablePagesScript
});
},
mounted() {
this.$nextTick(() => {
if (this.page.script && this.hpml.aiscript) {
let ast;
try {
ast = parse(this.page.script);
} catch (e) {
console.error(e);
/*os.dialog({
type: 'error',
text: 'Syntax error :('
});*/
return;
onMounted(() => {
nextTick(() => {
if (props.page.script && hpml.aiscript) {
let ast;
try {
ast = parse(props.page.script);
} catch (e) {
console.error(e);
/*os.dialog({
type: 'error',
text: 'Syntax error :('
});*/
return;
}
hpml.aiscript.exec(ast).then(() => {
hpml.eval();
}).catch(e => {
console.error(e);
/*os.dialog({
type: 'error',
text: e
});*/
});
} else {
hpml.eval();
}
this.hpml.aiscript.exec(ast).then(() => {
this.hpml.eval();
}).catch(e => {
console.error(e);
/*os.dialog({
type: 'error',
text: e
});*/
});
} else {
this.hpml.eval();
}
});
onUnmounted(() => {
if (hpml.aiscript) hpml.aiscript.abort();
});
});
},
beforeUnmount() {
if (this.hpml.aiscript) this.hpml.aiscript.abort();
return {
hpml,
};
},
});
</script>

View File

@@ -0,0 +1,18 @@
import { Directive } from 'vue';
export default {
beforeMount(src, binding, vn) {
src.style.opacity = '0';
src.style.transform = 'scale(0.9)';
// ページネーションと相性が悪いので
//if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`;
src.classList.add('_zoom');
},
mounted(src, binding, vn) {
setTimeout(() => {
src.style.opacity = '1';
src.style.transform = 'none';
}, 1);
},
} as Directive;

View File

@@ -6,6 +6,7 @@ import particle from './particle';
import tooltip from './tooltip';
import hotkey from './hotkey';
import appear from './appear';
import anim from './anim';
export default function(app: App) {
app.directive('userPreview', userPreview);
@@ -15,4 +16,5 @@ export default function(app: App) {
app.directive('tooltip', tooltip);
app.directive('hotkey', hotkey);
app.directive('appear', appear);
app.directive('anim', anim);
}

View File

@@ -64,6 +64,7 @@ import { fetchInstance, instance } from '@/instance';
import { makeHotkey } from './scripts/hotkey';
import { search } from './scripts/search';
import { getThemes } from './theme-store';
import { initializeSw } from './scripts/initialize-sw';
console.info(`Misskey v${version}`);
@@ -177,48 +178,7 @@ fetchInstance().then(() => {
localStorage.setItem('v', instance.version);
// Init service worker
if (instance.swPublickey &&
('serviceWorker' in navigator) &&
('PushManager' in window) &&
$i && $i.token) {
navigator.serviceWorker.register(`/sw.js`);
navigator.serviceWorker.ready.then(registration => {
registration.active?.postMessage({
msg: 'initialize',
lang,
});
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
}).then(subscription => {
function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
// Register
api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name === 'NotAllowedError') {
return;
}
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
// 既に存在していることが原因でエラーになった可能性があるので、
// そのサブスクリプションを解除しておく
const subscription = await registration.pushManager.getSubscription();
if (subscription) subscription.unsubscribe();
});
});
}
initializeSw();
});
stream.init($i);

View File

@@ -175,6 +175,7 @@
<MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $ts.objectStorageUseSSL }}<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $ts.objectStorageUseProxy }}<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $ts.objectStorageSetPublicRead }}</MkSwitch>
<MkSwitch v-model:value="objectStorageS3ForcePathStyle" :disabled="!useObjectStorage">s3ForcePathStyle</MkSwitch>
</template>
</div>
<div class="_footer">
@@ -325,6 +326,7 @@ export default defineComponent({
objectStorageUseSSL: false,
objectStorageUseProxy: false,
objectStorageSetPublicRead: false,
objectStorageS3ForcePathStyle: true,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
@@ -393,6 +395,7 @@ export default defineComponent({
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
this.objectStorageUseProxy = this.meta.objectStorageUseProxy;
this.objectStorageSetPublicRead = this.meta.objectStorageSetPublicRead;
this.objectStorageS3ForcePathStyle = this.meta.objectStorageS3ForcePathStyle;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
@@ -547,6 +550,7 @@ export default defineComponent({
objectStorageUseSSL: this.objectStorageUseSSL,
objectStorageUseProxy: this.objectStorageUseProxy,
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,

View File

@@ -10,6 +10,7 @@
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-index="i"
:key="message.id"
v-anim="i"
>
<div>
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>

View File

@@ -1,6 +1,6 @@
<template>
<div class="fcuexfpr">
<div v-if="note" class="note">
<div v-if="note" class="note" v-anim>
<div class="_section" v-if="showNext">
<XNotes class="_content _noGap_" :pagination="next"/>
</div>

View File

@@ -61,8 +61,10 @@ import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import { v4 as uuid } from 'uuid';
import XContainer from './page-editor.container.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import { isLiteralBlock, funcDefs, blockDefs } from '@/scripts/hpml/index';
import { blockDefs } from '@/scripts/hpml/index';
import * as os from '@/os';
import { isLiteralValue } from '@/scripts/hpml/expr';
import { funcDefs } from '@/scripts/hpml/lib';
export default defineComponent({
components: {
@@ -166,7 +168,7 @@ export default defineComponent({
return;
}
if (isLiteralBlock(this.value)) return;
if (isLiteralValue(this.value)) return;
const empties = [];
for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {

View File

@@ -20,7 +20,7 @@
</div>
<div class="_section links">
<div class="_content">
<MkA :to="`./${page.name}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>

View File

@@ -0,0 +1,32 @@
<template>
<div class="graojtoi">
<MkSample/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faEye } from '@fortawesome/free-solid-svg-icons';
import MkSample from '@/components/sample.vue';
export default defineComponent({
components: {
MkSample,
},
data() {
return {
INFO: {
title: this.$ts.preview,
icon: faEye,
},
}
},
});
</script>
<style lang="scss" scoped>
.graojtoi {
padding: var(--margin);
}
</style>

View File

@@ -8,6 +8,10 @@
{{ $i.email || $ts.notSet }}
</FormLink>
</FormGroup>
<FormSwitch :value="$i.receiveAnnouncementEmail" @update:value="onChangeReceiveAnnouncementEmail">
{{ $ts.receiveAnnouncementFromInstance }}
</FormSwitch>
</FormBase>
</template>
@@ -19,6 +23,7 @@ import FormButton from '@/components/form/button.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
export default defineComponent({
@@ -26,6 +31,7 @@ export default defineComponent({
FormBase,
FormLink,
FormButton,
FormSwitch,
FormGroup,
},
@@ -46,7 +52,11 @@ export default defineComponent({
},
methods: {
onChangeReceiveAnnouncementEmail(v) {
os.api('i/update', {
receiveAnnouncementEmail: v
});
},
}
});
</script>

View File

@@ -18,7 +18,7 @@
<FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $ts.theme }}</FormLink>
<FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $ts.sidebar }}</FormLink>
<FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $ts.sounds }}</FormLink>
<FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink>
<FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><Fa :icon="faPlug"/></template>{{ $ts.plugins }}</FormLink>
</FormGroup>
<FormGroup>
<template #label>{{ $ts.otherSettings }}</template>
@@ -105,7 +105,9 @@ export default defineComponent({
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'update': return defineAsyncComponent(() => import('./update.vue'));

View File

@@ -1,26 +1,32 @@
<template>
<section class="_section">
<div class="_content" v-if="enableTwitterIntegration">
<header><Fa :icon="faTwitter"/> Twitter</header>
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectTwitter">{{ $ts.connectSerice }}</MkButton>
<FormBase>
<div class="_formItem" v-if="enableTwitterIntegration">
<div class="_formLabel"><Fa :icon="faTwitter"/> Twitter</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
<div class="_content" v-if="enableDiscordIntegration">
<header><Fa :icon="faDiscord"/> Discord</header>
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discordapp.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectDiscord">{{ $ts.connectSerice }}</MkButton>
<div class="_formItem" v-if="enableDiscordIntegration">
<div class="_formLabel"><Fa :icon="faDiscord"/> Discord</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
<div class="_content" v-if="enableGithubIntegration">
<header><Fa :icon="faGithub"/> GitHub</header>
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub">{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectGithub">{{ $ts.connectSerice }}</MkButton>
<div class="_formItem" v-if="enableGithubIntegration">
<div class="_formLabel"><Fa :icon="faGithub"/> GitHub</div>
<div class="_formPanel" style="padding: 16px;">
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectSerice }}</MkButton>
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectSerice }}</MkButton>
</div>
</div>
</section>
</FormBase>
</template>
<script lang="ts">
@@ -28,11 +34,13 @@ import { defineComponent } from 'vue';
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import { apiUrl } from '@/config';
import FormBase from '@/components/form/base.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
MkButton
},

View File

@@ -0,0 +1,146 @@
<template>
<FormBase>
<MkInfo warn>{{ $ts.pluginInstallWarn }}</MkInfo>
<FormGroup>
<FormTextarea v-model:value="code" tall>
<span>{{ $ts.code }}</span>
</FormTextarea>
</FormGroup>
<FormButton @click="install" :disabled="code == null" primary inline><Fa :icon="faCheck"/> {{ $ts.install }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
MkInfo,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts._plugin.install,
icon: faDownload
},
code: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
},
async install() {
let ast;
try {
ast = parse(this.code);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.dialog({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(import('@/components/token-generate-window.vue'), {
title: this.$ts.tokenRequested,
information: this.$ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
}
}, 'closed');
});
this.installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
location.reload();
});
},
}
});
</script>

View File

@@ -0,0 +1,117 @@
<template>
<FormBase>
<FormGroup v-for="plugin in plugins" :key="plugin.id">
<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
<FormSwitch :value="plugin.active" @update:value="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
<div class="_formItem">
<div class="_formPanel" style="padding: 16px;">
<div class="_keyValue">
<div>{{ $ts.author }}:</div>
<div>{{ plugin.author }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.description }}:</div>
<div>{{ plugin.description }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.permission }}:</div>
<div>{{ plugin.permissions }}</div>
</div>
</div>
</div>
<div class="_formItem">
<div class="_formPanel" style="padding: 16px;">
<MkButton @click="config(plugin)" inline v-if="plugin.config"><Fa :icon="faCog"/> {{ $ts.settings }}</MkButton>
<MkButton @click="uninstall(plugin)" inline danger><Fa :icon="faTrashAlt"/> {{ $ts.uninstall }}</MkButton>
</div>
</div>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInfo from '@/components/ui/info.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkSelect,
MkInfo,
FormSwitch,
FormBase,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts._plugin.manage,
icon: faPlug
},
plugins: ColdDeviceStorage.get('plugins'),
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
uninstall(plugin) {
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
os.success();
this.$nextTick(() => {
location.reload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config(plugin) {
const config = plugin.config;
for (const key in plugin.configData) {
config[key].default = plugin.configData[key];
}
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).configData = result;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).active = active;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<FormBase>
<FormLink to="/settings/plugin/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._plugin.install }}</FormLink>
<FormLink to="/settings/plugin/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
FormBase,
FormLink,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$ts.plugins,
icon: faPlug
},
plugins: ColdDeviceStorage.get('plugins').length,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
mounted() {
this.$emit('info', this.INFO);
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,211 +0,0 @@
<template>
<section class="_section">
<div class="_title"><Fa :icon="faPlug"/> {{ $ts.plugins }}</div>
<div class="_content">
<details>
<summary><Fa :icon="faDownload"/> {{ $ts.install }}</summary>
<MkInfo warn>{{ $ts.pluginInstallWarn }}</MkInfo>
<MkTextarea v-model:value="script" tall>
<span>{{ $ts.script }}</span>
</MkTextarea>
<MkButton @click="install()" primary><Fa :icon="faSave"/> {{ $ts.install }}</MkButton>
</details>
</div>
<div class="_content">
<details>
<summary><Fa :icon="faFolderOpen"/> {{ $ts.manage }}</summary>
<MkSelect v-model:value="selectedPluginId">
<option v-for="x in plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedPlugin">
<div style="margin: -8px 0 8px 0;">
<MkSwitch :value="selectedPlugin.active" @update:value="changeActive(selectedPlugin, $event)">{{ $ts.makeActive }}</MkSwitch>
</div>
<div class="_keyValue">
<div>{{ $ts.version }}:</div>
<div>{{ selectedPlugin.version }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.author }}:</div>
<div>{{ selectedPlugin.author }}</div>
</div>
<div class="_keyValue">
<div>{{ $ts.description }}:</div>
<div>{{ selectedPlugin.description }}</div>
</div>
<div style="margin-top: 8px;">
<MkButton @click="config()" inline v-if="selectedPlugin.config"><Fa :icon="faCog"/> {{ $ts.settings }}</MkButton>
<MkButton @click="uninstall()" inline><Fa :icon="faTrashAlt"/> {{ $ts.uninstall }}</MkButton>
</div>
</template>
</details>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInfo from '@/components/ui/info.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkSelect,
MkInfo,
MkSwitch,
},
data() {
return {
script: '',
plugins: ColdDeviceStorage.get('plugins'),
selectedPluginId: null,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
computed: {
selectedPlugin() {
if (this.selectedPluginId == null) return null;
return this.plugins.find(x => x.id === this.selectedPluginId);
},
},
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', this.plugins.concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
},
async install() {
let ast;
try {
ast = parse(this.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.dialog({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(import('@/components/token-generate-window.vue'), {
title: this.$ts.tokenRequested,
information: this.$ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
}
}, 'closed');
});
this.installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
location.reload();
});
},
uninstall() {
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== this.selectedPluginId));
os.success();
this.$nextTick(() => {
location.reload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config() {
const config = this.selectedPlugin.config;
for (const key in this.selectedPlugin.configData) {
config[key].default = this.selectedPlugin.configData[key];
}
const { canceled, result } = await os.form(this.selectedPlugin.name, config);
if (canceled) return;
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === this.selectedPluginId).configData = result;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).active = active;
ColdDeviceStorage.set('plugins', plugins);
this.$nextTick(() => {
location.reload();
});
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -58,7 +58,7 @@
<FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>
</FormGroup>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}</FormLink>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
</FormBase>
</template>
@@ -106,6 +106,7 @@ export default defineComponent({
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(localStorage.getItem('wallpaper'));
const themesCount = installedThemes.value.length;
watch(darkTheme, () => {
if (defaultStore.state.darkMode) {
@@ -150,6 +151,7 @@ export default defineComponent({
lightTheme,
darkMode,
syncDeviceDarkMode,
themesCount,
wallpaper,
setWallpaper(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {

View File

@@ -4,12 +4,12 @@
<div class="_formLabel">{{ $ts.backgroundColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
<div class="preview" :style="{ background: color.forPreview }"></div>
</button>
</div>
@@ -19,7 +19,7 @@
<div class="_formLabel">{{ $ts.accentColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in accentColors" :key="color" @click="accentColor = color" class="color rounded _button" :class="{ active: accentColor === color }">
<button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }">
<div class="preview" :style="{ background: color }"></div>
</button>
</div>
@@ -29,34 +29,40 @@
<div class="_formLabel">{{ $ts.textColor }}</div>
<div class="_formPanel colors">
<div class="row">
<button v-for="color in fgColors" :key="color" @click="fgColor = color" class="color char _button" :class="{ active: fgColor === color }">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : bgColor?.kind === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
<button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
</button>
</div>
</div>
</div>
<div class="_formItem preview">
<div class="_formLabel">{{ $ts.preview }}</div>
<div class="_formPanel preview">
<MkSample class="preview"/>
</div>
</div>
<FormButton @click="saveAs" primary>{{ $ts.saveAs }}</FormButton>
<FormGroup v-if="codeEnabled">
<FormTextarea v-model:value="themeCode" tall>
<span>{{ $ts._theme.code }}</span>
</FormTextarea>
<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton>
</FormGroup>
<FormButton v-else @click="codeEnabled = true"><Fa :icon="faCode"/> {{ $ts.editCode }}</FormButton>
<FormGroup>
<FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton>
<FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
import { faPalette, faSave, faEye, faCode } from '@fortawesome/free-solid-svg-icons';
import { toUnicode } from 'punycode';
import * as tinycolor from 'tinycolor2';
import { v4 as uuid} from 'uuid';
import * as JSON5 from 'json5';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import MkSample from '@/components/sample.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormGroup from '@/components/form/group.vue';
import { Theme, applyTheme, validateTheme } from '@/scripts/theme';
import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@/scripts/theme';
import { host } from '@/config';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
@@ -66,7 +72,8 @@ export default defineComponent({
components: {
FormBase,
FormButton,
MkSample,
FormTextarea,
FormGroup,
},
data() {
@@ -75,6 +82,12 @@ export default defineComponent({
title: this.$ts.themeEditor,
icon: faPalette,
},
theme: {
base: 'light',
props: lightTheme.props
} as Theme,
codeEnabled: false,
themeCode: null,
bgColors: [
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
@@ -93,9 +106,7 @@ export default defineComponent({
{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
{ color: '#191919', kind: 'dark', forPreview: '#272727' },
],
bgColor: null,
accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
accentColor: null,
fgColors: [
{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
@@ -105,27 +116,13 @@ export default defineComponent({
{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
],
fgColor: null,
changed: false,
faPalette,
faPalette, faSave, faEye, faCode,
}
},
created() {
const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg');
const matchedBgColor = this.bgColors.find(x => tinycolor(x.color).toRgbString() === tinycolor(currentBgColor).toRgbString());
if (matchedBgColor) this.bgColor = matchedBgColor;
const currentAccentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent');
const matchedAccentColor = this.accentColors.find(x => tinycolor(x).toRgbString() === tinycolor(currentAccentColor).toRgbString());
if (matchedAccentColor) this.accentColor = matchedAccentColor;
const currentFgColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(currentFgColor).toRgbString()));
if (matchedFgColor) this.fgColor = matchedFgColor;
this.$watch('bgColor', this.apply);
this.$watch('accentColor', this.apply);
this.$watch('fgColor', this.apply);
this.$watch('theme', this.apply, { deep: true });
window.addEventListener('beforeunload', this.beforeunload);
},
@@ -156,28 +153,58 @@ export default defineComponent({
return !canceled;
},
convert(): Theme {
return {
name: this.$ts.myTheme,
base: this.bgColor.kind,
props: {
bg: this.bgColor.color,
fg: this.bgColor.kind === 'light' ? this.fgColor.forLight : this.fgColor.forDark,
accent: this.accentColor,
showPreview() {
os.pageWindow('preview');
},
setBgColor(color) {
if (this.theme.base != color.kind) {
const base = color.kind === 'dark' ? darkTheme : lightTheme;
for (const prop of Object.keys(base.props)) {
if (prop === 'accent') continue;
if (prop === 'fg') continue;
this.theme.props[prop] = base.props[prop];
}
};
}
this.theme.base = color.kind;
this.theme.props.bg = color.color;
if (this.theme.props.fg) {
const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
if (matchedFgColor) this.setFgColor(matchedFgColor);
}
},
setAccentColor(color) {
this.theme.props.accent = color;
},
setFgColor(color) {
this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
},
apply() {
if (this.bgColor == null) this.bgColor = this.bgColors[0];
if (this.accentColor == null) this.accentColor = this.accentColors[0];
if (this.fgColor == null) this.fgColor = this.fgColors[0];
const theme = this.convert();
applyTheme(theme, false);
this.themeCode = JSON5.stringify(this.theme, null, '\t');
applyTheme(this.theme, false);
this.changed = true;
},
applyThemeCode() {
let parsed;
try {
parsed = JSON5.parse(this.themeCode);
} catch (e) {
os.dialog({
type: 'error',
text: this.$ts._theme.invalid
});
return;
}
this.theme = parsed;
},
async saveAs() {
const { canceled, result: name } = await os.dialog({
title: this.$ts.name,
@@ -187,21 +214,20 @@ export default defineComponent({
});
if (canceled) return;
const theme = this.convert();
theme.id = uuid();
theme.name = name;
theme.author = `@${this.$i.username}@${toUnicode(host)}`;
addTheme(theme);
applyTheme(theme);
this.theme.id = uuid();
this.theme.name = name;
this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
addTheme(this.theme);
applyTheme(this.theme);
if (this.$store.state.darkMode) {
ColdDeviceStorage.set('darkTheme', theme.id);
ColdDeviceStorage.set('darkTheme', this.theme.id);
} else {
ColdDeviceStorage.set('lightTheme', theme.id);
ColdDeviceStorage.set('lightTheme', this.theme.id);
}
this.changed = false;
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
text: this.$t('_theme.installed', { name: this.theme.name })
});
}
}
@@ -265,10 +291,5 @@ export default defineComponent({
}
}
}
> .preview > .preview > .preview {
box-shadow: none;
background: transparent;
}
}
</style>

View File

@@ -39,7 +39,7 @@ export function install(plugin) {
function createPluginEnv(opts) {
const config = new Map();
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default));
}
return {

View File

@@ -76,6 +76,7 @@ export const router = createRouter({
{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
{ path: '/api-console', component: page('api-console') },
{ path: '/preview', component: page('preview') },
{ path: '/test', component: page('test') },
{ path: '/auth/:token', component: page('auth') },
{ path: '/miauth/:session', component: page('miauth') },

View File

@@ -0,0 +1,109 @@
// blocks
export type BlockBase = {
id: string;
type: string;
};
export type TextBlock = BlockBase & {
type: 'text';
text: string;
};
export type SectionBlock = BlockBase & {
type: 'section';
title: string;
children: (Block | VarBlock)[];
};
export type ImageBlock = BlockBase & {
type: 'image';
fileId: string | null;
};
export type ButtonBlock = BlockBase & {
type: 'button';
text: any;
primary: boolean;
action: string;
content: string;
event: string;
message: string;
var: string;
fn: string;
};
export type IfBlock = BlockBase & {
type: 'if';
var: string;
children: Block[];
};
export type TextareaBlock = BlockBase & {
type: 'textarea';
text: string;
};
export type PostBlock = BlockBase & {
type: 'post';
text: string;
attachCanvasImage: boolean;
canvasId: string;
};
export type CanvasBlock = BlockBase & {
type: 'canvas';
name: string; // canvas id
width: number;
height: number;
};
export type NoteBlock = BlockBase & {
type: 'note';
detailed: boolean;
note: string | null;
};
export type Block =
TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock;
// variable blocks
export type VarBlockBase = BlockBase & {
name: string;
};
export type NumberInputVarBlock = VarBlockBase & {
type: 'numberInput';
text: string;
};
export type TextInputVarBlock = VarBlockBase & {
type: 'textInput';
text: string;
};
export type SwitchVarBlock = VarBlockBase & {
type: 'switch';
text: string;
};
export type RadioButtonVarBlock = VarBlockBase & {
type: 'radioButton';
title: string;
values: string[];
};
export type CounterVarBlock = VarBlockBase & {
type: 'counter';
text: string;
inc: number;
};
export type VarBlock =
NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock;
const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter'];
export function isVarBlock(block: Block): block is VarBlock {
return varBlock.includes(block.type);
}

View File

@@ -1,12 +1,13 @@
import autobind from 'autobind-decorator';
import { Variable, PageVar, envVarsDef, Block, isFnBlock, Fn, HpmlScope, HpmlError } from '.';
import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.';
import { version } from '@/config';
import { AiScript, utils, values } from '@syuilo/aiscript';
import { createAiScriptEnv } from '../aiscript/api';
import { collectPageVars } from '../collect-page-vars';
import { initHpmlLib, initAiLib } from './lib';
import * as os from '@/os';
import { markRaw, ref, Ref } from 'vue';
import { markRaw, ref, Ref, unref } from 'vue';
import { Expr, isLiteralValue, Variable } from './expr';
/**
* Hpml evaluator
@@ -94,7 +95,7 @@ export class Hpml {
public interpolate(str: string) {
if (str == null) return null;
return str.replace(/{(.+?)}/g, match => {
const v = this.vars[match.slice(1, -1).trim()];
const v = unref(this.vars)[match.slice(1, -1).trim()];
return v == null ? 'NULL' : v.toString();
});
}
@@ -158,72 +159,76 @@ export class Hpml {
}
@autobind
private evaluate(block: Block, scope: HpmlScope): any {
if (block.type === null) {
return null;
}
private evaluate(expr: Expr, scope: HpmlScope): any {
if (block.type === 'number') {
return parseInt(block.value, 10);
}
if (block.type === 'text' || block.type === 'multiLineText') {
return this._interpolateScope(block.value || '', scope);
}
if (block.type === 'textList') {
return this._interpolateScope(block.value || '', scope).trim().split('\n');
}
if (block.type === 'ref') {
return scope.getState(block.value);
}
if (block.type === 'aiScriptVar') {
if (this.aiscript) {
try {
return utils.valToJs(this.aiscript.scope.get(block.value));
} catch (e) {
return null;
}
} else {
if (isLiteralValue(expr)) {
if (expr.type === null) {
return null;
}
}
// Define user function
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));
if (expr.type === 'number') {
return parseInt((expr.value as any), 10);
}
if (expr.type === 'text' || expr.type === 'multiLineText') {
return this._interpolateScope(expr.value || '', scope);
}
if (expr.type === 'textList') {
return this._interpolateScope(expr.value || '', scope).trim().split('\n');
}
if (expr.type === 'ref') {
return scope.getState(expr.value);
}
if (expr.type === 'aiScriptVar') {
if (this.aiscript) {
try {
return utils.valToJs(this.aiscript.scope.get(expr.value));
} catch (e) {
return null;
}
} else {
return null;
}
} as Fn;
}
// Define user function
if (expr.type == 'fn') {
return {
slots: expr.value.slots.map(x => x.name),
exec: (slotArg: Record<string, any>) => {
return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id));
}
} as Fn;
}
return;
}
// Call user function
if (block.type.startsWith('fn:')) {
const fnName = block.type.split(':')[1];
if (expr.type.startsWith('fn:')) {
const fnName = expr.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);
args[name] = this.evaluate(expr.args[i], scope);
}
return fn.exec(args);
}
if (block.args === undefined) return null;
if (expr.args === undefined) return null;
const funcs = initHpmlLib(block, scope, this.opts.randomSeed, this.opts.visitor);
const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor);
// Call function
const fnName = block.type;
const fnName = expr.type;
const fn = (funcs as any)[fnName];
if (fn == null) {
throw new HpmlError(`No such function '${fnName}'`);
} else {
return fn(...block.args.map(x => this.evaluate(x, scope)));
return fn(...expr.args.map(x => this.evaluate(x, scope)));
}
}
}

View File

@@ -0,0 +1,79 @@
import { literalDefs, Type } from '.';
export type ExprBase = {
id: string;
};
// value
export type EmptyValue = ExprBase & {
type: null;
value: null;
};
export type TextValue = ExprBase & {
type: 'text';
value: string;
};
export type MultiLineTextValue = ExprBase & {
type: 'multiLineText';
value: string;
};
export type TextListValue = ExprBase & {
type: 'textList';
value: string;
};
export type NumberValue = ExprBase & {
type: 'number';
value: number;
};
export type RefValue = ExprBase & {
type: 'ref';
value: string; // value is variable name
};
export type AiScriptRefValue = ExprBase & {
type: 'aiScriptVar';
value: string; // value is variable name
};
export type UserFnValue = ExprBase & {
type: 'fn';
value: UserFnInnerValue;
};
type UserFnInnerValue = {
slots: {
name: string;
type: Type;
}[];
expression: Expr;
};
export type Value =
EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue;
export function isLiteralValue(expr: Expr): expr is Value {
if (expr.type == null) return true;
if (literalDefs[expr.type]) return true;
return false;
}
// call function
export type CallFn = ExprBase & { // "fn:hoge" or string
type: string;
args: Expr[];
value: null;
};
// variable
export type Variable = (Value | CallFn) & {
name: string;
};
// expression
export type Expr = Variable | Value | CallFn;

View File

@@ -3,52 +3,16 @@
*/
import autobind from 'autobind-decorator';
import {
faMagic,
faSquareRootAlt,
faAlignLeft,
faShareAlt,
faPlus,
faMinus,
faTimes,
faDivide,
faList,
faQuoteRight,
faEquals,
faGreaterThan,
faLessThan,
faGreaterThanEqual,
faLessThanEqual,
faNotEqual,
faDice,
faSortNumericUp,
faExchangeAlt,
faRecycle,
faIndent,
faCalculator,
} from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
import { Hpml } from './evaluator';
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;
};
import { funcDefs } from './lib';
export type Fn = {
slots: string[];
@@ -57,46 +21,6 @@ export type Fn = {
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, },
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
round: { in: ['number'], out: 'number', category: 'operation', icon: faCalculator, },
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, },
pick: { in: [null, 'number'], out: null, category: 'list', icon: faIndent, },
listLen: { in: [null], out: 'number', category: 'list', icon: faIndent, },
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, },
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: faDice, }, // dailyRandomPickWithProbabilityMapping
};
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, },
@@ -116,10 +40,6 @@ export const blockDefs = [
}))
];
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> = {
@@ -140,12 +60,6 @@ export const envVarsDef: Record<string, Type> = {
NULL: null,
};
export function isLiteralBlock(v: Block) {
if (v.type === null) return true;
if (literalDefs[v.type]) return true;
return false;
}
export class HpmlScope {
private layerdStates: Record<string, any>[];
public name: string;

View File

@@ -2,9 +2,31 @@ import * as tinycolor from 'tinycolor2';
import Chart from 'chart.js';
import { Hpml } from './evaluator';
import { values, utils } from '@syuilo/aiscript';
import { Block, Fn, HpmlScope } from '.';
import { Fn, HpmlScope } from '.';
import { Expr } from './expr';
import * as seedrandom from 'seedrandom';
import {
faShareAlt,
faPlus,
faMinus,
faTimes,
faDivide,
faQuoteRight,
faEquals,
faGreaterThan,
faLessThan,
faGreaterThanEqual,
faLessThanEqual,
faNotEqual,
faDice,
faExchangeAlt,
faRecycle,
faIndent,
faCalculator,
} from '@fortawesome/free-solid-svg-icons';
import { faFlag } from '@fortawesome/free-regular-svg-icons';
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
Chart.pluginService.register({
beforeDraw: (chart, easing) => {
@@ -125,7 +147,47 @@ export function initAiLib(hpml: Hpml) {
};
}
export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string, visitor?: any) {
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, },
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
round: { in: ['number'], out: 'number', category: 'operation', icon: faCalculator, },
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, },
pick: { in: [null, 'number'], out: null, category: 'list', icon: faIndent, },
listLen: { in: [null], out: 'number', category: 'list', icon: faIndent, },
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, },
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: faDice, }, // dailyRandomPickWithProbabilityMapping
};
export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
const date = new Date();
const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
@@ -166,12 +228,12 @@ export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string,
splitStrByLine: (a: string) => a.split('\n'),
pick: (list: any[], i: number) => list[i - 1],
listLen: (list: any[]) => list.length,
random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * 100) < probability,
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * (max - min + 1)),
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${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)],
random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability,
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)),
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)],
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.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)],
@@ -185,7 +247,7 @@ export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string,
totalFactor += factor;
xs.push({ factor, text });
}
const r = seedrandom(`${day}:${block.id}`)() * totalFactor;
const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
let stackedFactor = 0;
for (const x of xs) {
if (r >= stackedFactor && r <= stackedFactor + x.factor) {

View File

@@ -1,5 +1,7 @@
import autobind from 'autobind-decorator';
import { Type, Block, funcDefs, envVarsDef, Variable, PageVar, isLiteralBlock } from '.';
import { Type, envVarsDef, PageVar } from '.';
import { Expr, isLiteralValue, Variable } from './expr';
import { funcDefs } from './lib';
type TypeError = {
arg: number;
@@ -20,10 +22,10 @@ export class HpmlTypeChecker {
}
@autobind
public typeCheck(v: Block): TypeError | null {
if (isLiteralBlock(v)) return null;
public typeCheck(v: Expr): TypeError | null {
if (isLiteralValue(v)) return null;
const def = funcDefs[v.type];
const def = funcDefs[v.type || ''];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
@@ -58,8 +60,8 @@ export class HpmlTypeChecker {
}
@autobind
public getExpectedType(v: Block, slot: number): Type {
const def = funcDefs[v.type];
public getExpectedType(v: Expr, slot: number): Type {
const def = funcDefs[v.type || ''];
if (def == null) {
throw new Error('Unknown type: ' + v.type);
}
@@ -86,7 +88,7 @@ export class HpmlTypeChecker {
}
@autobind
public infer(v: Block): Type {
public infer(v: Expr): Type {
if (v.type === null) return null;
if (v.type === 'text') return 'string';
if (v.type === 'multiLineText') return 'string';
@@ -103,7 +105,7 @@ export class HpmlTypeChecker {
return pageVar.type;
}
const envVar = envVarsDef[v.value];
const envVar = envVarsDef[v.value || ''];
if (envVar !== undefined) {
return envVar;
}

View File

@@ -0,0 +1,68 @@
import { instance } from '@/instance';
import { $i } from '@/account';
import { api } from '@/os';
import { lang } from '@/config';
export async function initializeSw() {
if (instance.swPublickey &&
('serviceWorker' in navigator) &&
('PushManager' in window) &&
$i && $i.token) {
navigator.serviceWorker.register(`/sw.js`);
navigator.serviceWorker.ready.then(registration => {
registration.active?.postMessage({
msg: 'initialize',
lang,
});
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
}).then(subscription => {
function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
// Register
api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name === 'NotAllowedError') {
return;
}
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
// 既に存在していることが原因でエラーになった可能性があるので、
// そのサブスクリプションを解除しておく
const subscription = await registration.pushManager.getSubscription();
if (subscription) subscription.unsubscribe();
});
});
}
}
/**
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

View File

@@ -481,6 +481,12 @@ hr {
outline: none;
}
._zoom {
transition-duration: 0.5s, 0.5s;
transition-property: opacity, transform;
transition-timing-function: cubic-bezier(0,.5,.5,1);
}
.zoom-enter-active, .zoom-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}

View File

@@ -68,6 +68,7 @@ self.addEventListener('activate', ev => {
});
//#endregion
// TODO: 消せるかも ref. https://github.com/syuilo/misskey/pull/7108#issuecomment-774573666
//#region When: Fetching
self.addEventListener('fetch', ev => {
if (ev.request.method !== 'GET' || ev.request.url.startsWith(apiUrl)) return;

View File

@@ -57,6 +57,13 @@ export default defineComponent({
},
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;

View File

@@ -187,6 +187,13 @@ export default defineComponent({
},
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;

View File

@@ -0,0 +1,164 @@
<template>
<MkContainer :show-header="props.showHeader">
<template #header><Fa :icon="faTerminal"/>{{ $ts._widgets.aiscript }}</template>
<div class="uylguesu _monospace">
<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
<button @click="run" class="_buttonPrimary">RUN</button>
<div class="logs">
<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
</div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTerminal } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '@/components/ui/container.vue';
import define from './define';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
const widget = define({
name: 'aiscript',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
script: {
type: 'string',
multiline: true,
default: '(1 + 1)',
hidden: true,
},
})
});
export default defineComponent({
extends: widget,
components: {
MkContainer
},
data() {
return {
logs: [],
faTerminal
};
},
methods: {
async run() {
this.logs = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'widget'
}), {
in: (q) => {
return new Promise(ok => {
os.dialog({
title: q,
input: {}
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
this.logs.push({
id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true
});
},
log: (type, params) => {
switch (type) {
case 'end': this.logs.push({
id: Math.random(),
text: utils.valToString(params.val, true),
print: false
}); break;
default: break;
}
}
});
let ast;
try {
ast = parse(this.props.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.dialog({
type: 'error',
text: e
});
}
},
}
});
</script>
<style lang="scss" scoped>
.uylguesu {
text-align: right;
> textarea {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid 1px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
&:focus {
outline: none;
}
}
> button {
display: inline-block;
margin: 8px;
padding: 0 10px;
height: 28px;
outline: none;
border-radius: 4px;
&:disabled {
opacity: 0.7;
cursor: default;
}
}
> .logs {
border-top: solid 1px var(--divider);
text-align: left;
padding: 16px;
&:empty {
display: none;
}
> .log {
&:not(.print) {
opacity: 0.7;
}
}
}
}
</style>

View File

@@ -18,6 +18,7 @@ export default function(app: App) {
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
}
export const widgets = [
@@ -38,4 +39,5 @@ export const widgets = [
'onlineUsers',
'jobQueue',
'button',
'aiscript',
];

View File

@@ -74,12 +74,18 @@ export default defineComponent({
max-width: 100%;
min-width: 100%;
padding: 16px;
color: var(--inputText);
background: var(--face);
color: var(--fg);
background: transparent;
border: none;
border-bottom: solid var(--lineWidth) var(--faceDivider);
border-bottom: solid 1px var(--divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
font-size: 0.9em;
&:focus {
outline: none;
}
}
> button {

View File

@@ -20,7 +20,7 @@ const path = process.env.NODE_ENV === 'test'
: `${dir}/default.yml`;
export default function load() {
const config = yaml.safeLoad(fs.readFileSync(path, 'utf-8')) as Source;
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin;

View File

@@ -33,7 +33,7 @@ The shortcuts listed here can be used basically everywhere.
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>Delete post</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>Open post context menu</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Toggle show or hide of content marked with CW</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>Unfocus</td><td>-</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>Deselect Note</td><td>-</td></tr>
</tbody>
</table>

View File

@@ -10,7 +10,7 @@
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>検索</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Cerca</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>

View File

@@ -1,4 +1,4 @@
# ミュート
# Silenzia
ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:

View File

@@ -322,33 +322,33 @@ Misskey提供一种被称为“帖子抓取”的机制。该功能以流的形
当您被某人关注时会触发该事件。
## `homeTimeline`
ホームタイムラインの投稿情報が流れてきます。该频道没有参数。
首页的时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
タイムラインに新しい投稿が流れてきたときに発生するイベントです
当时间线有新帖子时触发此事件
## `localTimeline`
ローカルタイムラインの投稿情報が流れてきます。该频道没有参数。
本地的时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです
当本地的时间线有新帖子时触发此事件
## `hybridTimeline`
ソーシャルタイムラインの投稿情報が流れてきます。该频道没有参数。
社交时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
ソーシャルタイムラインに新しい投稿が流れてきたときに発生するイベントです
当社交时间线有新帖子时触发此事件
## `globalTimeline`
グローバルタイムラインの投稿情報が流れてきます。该频道没有参数。
全局时间线上发布的信息将会传到这里。该频道没有参数。
### 发送的事件列表
#### `note`
グローバルタイムラインに新しい投稿が流れてきたときに発生するイベントです
全局时间线有新帖子时触发此事件

View File

@@ -31,8 +31,6 @@
},
}
```
* `id` ... 该主题的唯一 ID推荐采用 UUID。

View File

@@ -1,2 +1,2 @@
# 自訂表情符號
カスタム絵文字は、インスタンスで用意された画像を絵文字のように使える機能です。 ノート、リアクション、チャット、自己紹介、名前などの場所で使うことができます。 カスタム絵文字をそれらの場所で使うには、絵文字ピッカーボタン(ある場合)を押すか、`:`を入力して絵文字サジェストを表示します。 テキスト内に`:foo:`のような形式の文字列が見つかると、`foo`部分がカスタム絵文字名と解釈され、表示時には対応したカスタム絵文字に置き換わります
表情符號功能可以讓您在各個地方使用預置的圖像表情。 它可以用於發帖、回應、聊天、自我介紹和姓名等地方。 要在這些位置使用自定義表情符號,請按表情符號選擇按鈕(如果有)或鍵入`:`以顯示表情符號建議。 如果在文本中找到格式為`:foo:`的字符串,則將`foo`部分解釋為自定義表情符號名稱,並在顯示時替換為相應的自定義表情符號

View File

@@ -1,2 +1,2 @@
# 追隨/解除追隨
當你追隨其他使用者時,你的時間軸上將出現他們的文。但是,他們對其他用戶的回覆不會被顯示。 若要追隨一個使用者,請點選其使用者頁面上的「追隨」按鈕。若要解除追隨,請再次點選「追隨」按鈕。
當你追隨其他使用者時,你的時間軸上將出現他們的文。但是,他們對其他用戶的回覆不會被顯示。 若要追隨一個使用者,請點選其使用者頁面上的「追隨」按鈕。若要解除追隨,請再次點選「追隨」按鈕。

View File

@@ -1,14 +1,14 @@
# キーボードショートカット
# 鍵盤快速鍵
## 公開
これらのショートカットは基本的にどこでも使えます
這些快捷方式基本上可以在任何地方使用
<table>
<thead>
<tr><th>快速鍵</th><th>功能</th><th>由來</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>發佈</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>發佈</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>轉跳至時間軸最新發佈的內容</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>顯示/隱藏通知</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>搜尋</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>取得說明</td><td><b>H</b>elp</td></tr>
@@ -29,11 +29,11 @@
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>お気に入りに登録</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>刪除</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>加入至我的最愛</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>刪除</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>顯示貼文選單</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>隱藏或顯示敏感媒體</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>取消選取</td><td>-</td></tr>
</tbody>
</table>
@@ -44,7 +44,7 @@
<tr><th>快速鍵</th><th>功能</th><th>由來</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">Enter</kbd></td><td>轉發</td><td>-</td></tr>
<tr><td><kbd class="key">Enter</kbd></td><td>轉發</td><td>-</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>展開選單</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>關閉選單</td><td>-</td></tr>
</tbody>

View File

@@ -1,13 +1,13 @@
# 靜音
ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
當你靜音某個帳戶時Misskey將停止推播該帳戶的以下內容
* タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote)
* そのユーザーからの通知
* メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
* 該用戶在時間軸及搜尋結果中的貼文(以及對這些貼文的回覆和轉發)。
* 該使用者的通知
* 使用者在訊息歷史記錄列表中的訊息歷史記錄
ユーザーをミュートするには、対象のユーザーのユーザーページに表示されている「ミュート」ボタンを押します
要靜音某個帳戶,可於其用戶檔案頁上點選**靜音**按鈕
ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません
被您靜音的用戶不會被通知已被靜音,也不會知道您已靜音他們
設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます
您可在**設定**>**靜音**中瀏覽已被您靜音的用戶

View File

@@ -1,15 +1,15 @@
# 不同時間軸之間的分別
# 不同時間軸的差異
https://docs.google.com/spreadsheets/d/1lxQ2ugKrhz58Bg96HTDK_2F98BUritkMyIiBkOByjHA/edit?usp=sharing
## 首頁
自分のフォローしているユーザーの投稿
顯示已追隨使用者的貼文
## 本地
全てのローカルユーザーの「ホーム」指定されていない投稿
顯示所有的本地用戶的首頁貼文
## 社群
自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
顯示已追隨使用者的貼文及所有的本地用戶的貼文
## 公開
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿
顯示已追隨使用者、所有的本地用戶及遠端使用者所發佈的的貼文

View File

@@ -1,8 +1,10 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { urlRegexFull } from './prelude';
import * as parse5 from 'parse5';
import treeAdapter = require('parse5/lib/tree-adapters/default');
import { URL } from 'url';
import { urlRegex, urlRegexFull } from './prelude';
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
const dom = parse5.parseFragment(html);
let text = '';
@@ -12,30 +14,35 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return text.trim();
function getText(node: any): string {
if (node.nodeName === '#text') return node.value;
function getText(node: parse5.Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.childNodes) {
return node.childNodes.map((n: any) => getText(n)).join('');
return node.childNodes.map(n => getText(n)).join('');
}
return '';
}
function analyze(node: any) {
switch (node.nodeName) {
case '#text':
text += node.value;
break;
function analyze(node: parse5.Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
return;
}
// Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) {
case 'br':
text += '\n';
break;
case 'a':
const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name === 'rel');
const href = node.attrs.find((x: any) => x.name === 'href');
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
@@ -44,7 +51,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length === 2) {
if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
@@ -54,11 +61,28 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
}
// その他
} else {
text += !href ? txt
: txt === href.value
? txt.match(urlRegexFull) ? txt
: `<${txt}>`
: `[${txt}](${href.value})`;
const generateLink = () => {
if (!href && !txt) {
return '';
}
if (!href) {
return txt;
}
if (!txt || txt === href.value) { // #6383: Missing text node
if (href.value.match(urlRegexFull)) {
return href.value;
} else {
return `<${href.value}>`;
}
}
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
return `[${txt}](<${href.value}>)`; // #6846
} else {
return `[${txt}](${href.value})`;
}
};
text += generateLink();
}
break;

56
src/misc/captcha.ts Normal file
View File

@@ -0,0 +1,56 @@
import fetch from 'node-fetch';
import { URLSearchParams } from 'url';
import { getAgentByUrl } from './fetch';
import config from '../config';
export async function verifyRecaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
throw `recaptcha-request-failed: ${e}`;
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
throw `recaptcha-failed: ${errorCodes}`;
}
}
export async function verifyHcaptcha(secret: string, response: string) {
const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
throw `hcaptcha-request-failed: ${e}`;
});
if (result.success !== true) {
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
throw `hcaptcha-failed: ${errorCodes}`;
}
}
type CaptchaResponse = {
success: boolean;
'error-codes'?: string[];
};
async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
const params = new URLSearchParams({
secret,
response
});
const res = await fetch(url, {
method: 'POST',
body: params,
headers: {
'User-Agent': config.userAgent
},
timeout: 10 * 1000,
agent: getAgentByUrl
}).catch(e => {
throw `${e.message || e}`;
});
if (!res.ok) {
throw `${res.status}`;
}
return await res.json() as CaptchaResponse;
}

View File

@@ -1,13 +1,13 @@
export function isMutedUserRelated(note: any, mutedUserIds: string[]): boolean {
if (mutedUserIds.includes(note.userId)) {
export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
if (mutedUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && mutedUserIds.includes(note.reply.userId)) {
if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && mutedUserIds.includes(note.renote.userId)) {
if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
return true;
}

View File

@@ -0,0 +1,6 @@
export function normalizeForSearch(tag: string): string {
// ref.
// - https://analytics-note.xyz/programming/unicode-normalization-forms/
// - https://maku77.github.io/js/string/normalize.html
return tag.normalize('NFKC').toLowerCase();
}

View File

@@ -399,4 +399,9 @@ export class Meta {
default: false,
})
public objectStorageSetPublicRead: boolean;
@Column('boolean', {
default: true,
})
public objectStorageS3ForcePathStyle: boolean;
}

View File

@@ -133,6 +133,11 @@ export class UserProfile {
})
public injectFeaturedNote: boolean;
@Column('boolean', {
default: true,
})
public receiveAnnouncementEmail: boolean;
@Column({
...id(),
nullable: true

View File

@@ -201,6 +201,12 @@ export class User {
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'
})
public followersUri: string | null;
@Index({ unique: true })
@Column('char', {
length: 16, nullable: true, unique: true,

View File

@@ -236,6 +236,7 @@ export class UserRepository extends Repository<User> {
avatarId: user.avatarId,
bannerId: user.bannerId,
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,

View File

@@ -86,8 +86,7 @@ function isPublic(id: string) {
}
function isFollowers(id: string, actor: IRemoteUser) {
return [
`${actor.uri}/followers`,
// actor.followerUri, // TODO
].includes(id);
return (
id === (actor.followersUri || `${actor.uri}/followers`)
);
}

View File

@@ -27,6 +27,7 @@ import { getConnection } from 'typeorm';
import { ensure } from '../../../prelude/ensure';
import { toArray } from '../../../prelude/array';
import { fetchInstanceMetadata } from '../../../services/fetch-instance-metadata';
import { normalizeForSearch } from '../../../misc/normalize-for-search';
const logger = apLogger;
@@ -134,7 +135,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const isBot = object.type === 'Service';
@@ -159,6 +160,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
host,
inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured ? getApId(person.featured) : undefined,
uri: person.id,
tags,
@@ -322,7 +324,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
@@ -330,6 +332,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
lastFetchedAt: new Date(),
inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined,
featured: person.featured,
emojis: emojiNames,
name: person.name,

View File

@@ -438,7 +438,11 @@ export const meta = {
objectStorageSetPublicRead: {
validator: $.optional.bool
}
},
objectStorageS3ForcePathStyle: {
validator: $.optional.bool
},
}
};
@@ -713,6 +717,10 @@ export default define(meta, async (ps, me) => {
set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead;
}
if (ps.objectStorageS3ForcePathStyle !== undefined) {
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
}
await getConnection().transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, {
order: {

View File

@@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { Hashtags } from '../../../../models';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
export const meta = {
desc: {
@@ -38,7 +39,7 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const hashtag = await Hashtags.findOne({ name: ps.tag.toLowerCase() });
const hashtag = await Hashtags.findOne({ name: normalizeForSearch(ps.tag) });
if (hashtag == null) {
throw new ApiError(meta.errors.noSuchHashtag);
}

View File

@@ -4,6 +4,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { safeForSql } from '../../../../misc/safe-for-sql';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
@@ -54,7 +55,7 @@ export const meta = {
export default define(meta, async () => {
const instance = await fetchMeta(true);
const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase());
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
const now = new Date(); // 5分単位で丸めた現在日時
now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0);

View File

@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
import { Users } from '../../../../models';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
export const meta = {
requireCredential: false as const,
@@ -59,7 +60,7 @@ export const meta = {
export default define(meta, async (ps, me) => {
const query = Users.createQueryBuilder('user')
.where(':tag = ANY(user.tags)', { tag: ps.tag.toLowerCase() });
.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) });
const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));

View File

@@ -15,6 +15,7 @@ import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile';
import { ensure } from '../../../../prelude/ensure';
import { notificationTypes } from '../../../../types';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
export const meta = {
desc: {
@@ -135,6 +136,10 @@ export const meta = {
validator: $.optional.bool,
},
receiveAnnouncementEmail: {
validator: $.optional.bool,
},
alwaysMarkNsfw: {
validator: $.optional.bool,
desc: {
@@ -219,6 +224,7 @@ export default define(meta, async (ps, user, token) => {
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.avatarId) {
@@ -281,7 +287,7 @@ export default define(meta, async (ps, user, token) => {
if (newDescription != null) {
const tokens = parse(newDescription);
emojis = emojis.concat(extractEmojis(tokens!));
tags = extractHashtags(tokens!).map(tag => tag.toLowerCase()).splice(0, 32);
tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
}
updates.emojis = emojis;

View File

@@ -205,6 +205,7 @@ export default define(meta, async (ps, me) => {
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle;
}
}

View File

@@ -7,6 +7,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { Brackets } from 'typeorm';
import { safeForSql } from '../../../../misc/safe-for-sql';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
export const meta = {
desc: {
@@ -101,7 +102,7 @@ export default define(meta, async (ps, me) => {
if (ps.tag) {
if (!safeForSql(ps.tag)) return;
query.andWhere(`'{"${ps.tag.toLowerCase()}"}' <@ note.tags`);
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
let i = 0;
query.andWhere(new Brackets(qb => {
@@ -109,7 +110,7 @@ export default define(meta, async (ps, me) => {
qb.orWhere(new Brackets(qb => {
for (const tag of tags) {
if (!safeForSql(tag)) return;
qb.andWhere(`'{"${tag.toLowerCase()}"}' <@ note.tags`);
qb.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
i++;
}
}));

View File

@@ -1,7 +1,6 @@
import * as Koa from 'koa';
import { fetchMeta } from '../../../misc/fetch-meta';
import { verify } from 'hcaptcha';
import * as recaptcha from 'recaptcha-promise';
import { verifyHcaptcha, verifyRecaptcha } from '../../../misc/captcha';
import { Users, RegistrationTickets } from '../../../models';
import { signup } from '../common/signup';
@@ -14,26 +13,15 @@ export default async (ctx: Koa.Context) => {
// ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') {
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then(
({ success }) => success,
() => false,
);
if (!success) {
ctx.throw(400, 'hcaptcha-failed');
}
await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
ctx.throw(400, e);
});
}
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
recaptcha.init({
secret_key: instance.recaptchaSecretKey
await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
ctx.throw(400, e);
});
const success = await recaptcha(body['g-recaptcha-response']);
if (!success) {
ctx.throw(400, 'recaptcha-failed');
}
}
}

View File

@@ -70,7 +70,7 @@ async function getOAuth2() {
return new OAuth2(
meta.discordClientId!,
meta.discordClientSecret!,
'https://discordapp.com/',
'https://discord.com/',
'api/oauth2/authorize',
'api/oauth2/token');
} else {
@@ -174,7 +174,7 @@ router.get('/dc/cb', async ctx => {
}
}));
const { id, username, discriminator } = await getJson('https://discordapp.com/api/users/@me', '*/*', 10 * 1000, {
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
'Authorization': `Bearer ${accessToken}`,
});
@@ -245,7 +245,7 @@ router.get('/dc/cb', async ctx => {
}
}));
const { id, username, discriminator } = await getJson('https://discordapp.com/api/users/@me', '*/*', 10 * 1000, {
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
'Authorization': `Bearer ${accessToken}`,
});
if (!id || !username || !discriminator) {

View File

@@ -3,6 +3,7 @@ import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
import Channel from '../channel';
import { Notes } from '../../../../models';
import { PackedNote } from '../../../../models/repositories/note';
import { normalizeForSearch } from '../../../../misc/normalize-for-search';
export default class extends Channel {
public readonly chName = 'hashtag';
@@ -23,7 +24,7 @@ export default class extends Channel {
@autobind
private async onNote(note: PackedNote) {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
// Renoteなら再pack

View File

@@ -19,10 +19,10 @@ export default class extends Channel {
@autobind
private async onNote(note: PackedNote) {
if (note.channelId) {
if (!this.followingChannels.includes(note.channelId)) return;
if (!this.followingChannels.has(note.channelId)) return;
} else {
// その投稿のユーザーをフォローしていなかったら弾く
if ((this.user!.id !== note.userId) && !this.following.includes(note.userId)) return;
if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
}
if (['followers', 'specified'].includes(note.visibility)) {

View File

@@ -29,9 +29,9 @@ export default class extends Channel {
// フォローしているチャンネルの投稿 の場合だけ
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
(note.channelId == null && this.following.includes(note.userId)) ||
(note.channelId == null && this.following.has(note.userId)) ||
(note.channelId == null && ((note.user as PackedUser).host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.includes(note.channelId))
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
if (['followers', 'specified'].includes(note.visibility)) {

View File

@@ -27,7 +27,7 @@ export default class extends Channel {
private async onNote(note: PackedNote) {
if ((note.user as PackedUser).host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.includes(note.channelId)) return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// リプライなら再pack
if (note.replyId != null) {

View File

@@ -16,7 +16,7 @@ export default class extends Channel {
switch (type) {
case 'notification': {
if (this.muting.includes(body.userId)) return;
if (this.muting.has(body.userId)) return;
if (body.note && body.note.isHidden) {
body.note = await Notes.pack(body.note.id, this.user, {
detail: true
@@ -25,7 +25,7 @@ export default class extends Channel {
break;
}
case 'mention': {
if (this.muting.includes(body.userId)) return;
if (this.muting.has(body.userId)) return;
if (body.isHidden) {
body = await Notes.pack(body.id, this.user, {
detail: true

View File

@@ -19,9 +19,9 @@ import { UserProfile } from '../../../models/entities/user-profile';
export default class Connection {
public user?: User;
public userProfile?: UserProfile;
public following: User['id'][] = [];
public muting: User['id'][] = [];
public followingChannels: ChannelModel['id'][] = [];
public following: Set<User['id']> = new Set();
public muting: Set<User['id']> = new Set();
public followingChannels: Set<ChannelModel['id']> = new Set();
public token?: AccessToken;
private wsConnection: websocket.connection;
public subscriber: EventEmitter;
@@ -267,7 +267,7 @@ export default class Connection {
select: ['followeeId']
});
this.following = followings.map(x => x.followeeId);
this.following = new Set<string>(followings.map(x => x.followeeId));
}
@autobind
@@ -279,7 +279,7 @@ export default class Connection {
select: ['muteeId']
});
this.muting = mutings.map(x => x.muteeId);
this.muting = new Set<string>(mutings.map(x => x.muteeId));
}
@autobind
@@ -291,7 +291,7 @@ export default class Connection {
select: ['followeeId']
});
this.followingChannels = followings.map(x => x.followeeId);
this.followingChannels = new Set<string>(followings.map(x => x.followeeId));
}
@autobind

View File

@@ -40,7 +40,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: U
_note.renote = await Notes.findOne(note.renoteId).then(ensure);
}
if (isMutedUserRelated(_note, mutings.map(x => x.muteeId))) {
if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}

View File

@@ -25,7 +25,14 @@ export async function createSystemUser(username: string) {
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
account = await transactionalEntityManager.save(new User({
const exist = await transactionalEntityManager.findOne(User, {
usernameLower: username.toLowerCase(),
host: null
});
if (exist) throw new Error('the user is already exists');
account = await transactionalEntityManager.insert(User, {
id: genId(),
createdAt: new Date(),
username: username,
@@ -36,24 +43,24 @@ export async function createSystemUser(username: string) {
isLocked: true,
isExplorable: false,
isBot: true,
}));
}).then(x => transactionalEntityManager.findOneOrFail(User, x.identifiers[0]));
await transactionalEntityManager.save(new UserKeypair({
await transactionalEntityManager.insert(UserKeypair, {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id
}));
});
await transactionalEntityManager.save(new UserProfile({
await transactionalEntityManager.insert(UserProfile, {
userId: account.id,
autoAcceptFollowed: false,
password: hash,
}));
});
await transactionalEntityManager.save(new UsedUsername({
await transactionalEntityManager.insert(UsedUsername, {
createdAt: new Date(),
username: username.toLowerCase(),
}));
});
});
return account;

View File

@@ -13,7 +13,9 @@ export function getS3(meta: Meta) {
secretAccessKey: meta.objectStorageSecretKey!,
region: meta.objectStorageRegion || undefined,
sslEnabled: meta.objectStorageUseSSL,
s3ForcePathStyle: !!meta.objectStorageEndpoint,
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
? false
: meta.objectStorageS3ForcePathStyle,
httpOptions: {
agent: getAgentByUrl(new URL(u), !meta.objectStorageUseProxy)
}

View File

@@ -33,6 +33,7 @@ import { addNoteToAntenna } from '../add-note-to-antenna';
import { countSameRenotes } from '../../misc/count-same-renotes';
import { deliverToRelays } from '../relay';
import { Channel } from '../../models/entities/channel';
import { normalizeForSearch } from '../../misc/normalize-for-search';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -460,7 +461,7 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
text: data.text,
hasPoll: data.poll != null,
cw: data.cw == null ? null : data.cw,
tags: tags.map(tag => tag.toLowerCase()),
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
userId: user.id,
viaMobile: data.viaMobile!,
@@ -547,7 +548,7 @@ function index(note: Note) {
index: config.elasticsearch.index || 'misskey_note',
id: note.id.toString(),
body: {
text: note.text.toLowerCase(),
text: normalizeForSearch(note.text),
userId: note.userId,
userHost: note.userHost
}

View File

@@ -3,6 +3,7 @@ import { Hashtags, Users } from '../models';
import { hashtagChart } from './chart';
import { genId } from '../misc/gen-id';
import { Hashtag } from '../models/entities/hashtag';
import { normalizeForSearch } from '../misc/normalize-for-search';
export async function updateHashtags(user: User, tags: string[]) {
for (const tag of tags) {
@@ -21,7 +22,7 @@ export async function updateUsertags(user: User, tags: string[]) {
}
export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) {
tag = tag.toLowerCase();
tag = normalizeForSearch(tag);
const index = await Hashtags.findOne({ name: tag });