Compare commits

...

78 Commits
2.1.2 ... 2.6.2

Author SHA1 Message Date
12c624fa58 2.6.2 2018-05-14 14:16:56 +09:00
97c4758de2 Use _id instead of createdAt to improve performance 2018-05-14 13:58:44 +09:00
f20c08f0f7 Fix bug 2018-05-14 13:54:18 +09:00
1641d6bce2 Update add-file.ts 2018-05-14 12:47:33 +09:00
f5d2cb5c61 Improve performance 2018-05-14 09:24:49 +09:00
26941f62c6 2.6.1 2018-05-14 09:15:49 +09:00
06461bb9ee NoteのuserIdに対してインデックスを張るように 2018-05-14 09:15:43 +09:00
9f4624283d Disable prev/next 2018-05-14 09:01:37 +09:00
d4b696d03a Fix bug 2018-05-13 17:00:34 +09:00
219fdecc50 Fix bug 2018-05-13 16:52:47 +09:00
7af9ad9869 Better error handling 2018-05-13 16:26:11 +09:00
a858dd4453 2.6.0 2018-05-13 03:17:09 +09:00
f47377d181 ✌️ 2018-05-13 03:16:31 +09:00
bc197bc958 Add index 2018-05-13 03:13:55 +09:00
1836dd7312 Merge branch 'master' of https://github.com/syuilo/misskey 2018-05-13 03:08:33 +09:00
b844a8e9d5 Add note 2018-05-13 03:08:00 +09:00
a4ed163b62 Merge pull request #1576 from mei23/mei-preview
Fix url preview
2018-05-10 06:36:19 +09:00
f40e1ff0cc Fix cause error in case preview has data URI 2018-05-10 01:08:33 +09:00
d261fdbbc0 Fix can't preview some url 2018-05-09 20:14:34 +09:00
6b0a42af27 Merge pull request #1575 from mei23/mei-listlm
Fix list load-more is not working
2018-05-09 05:15:51 +09:00
107d9fd2c8 Fix list load-more is not working 2018-05-09 04:56:07 +09:00
4116b9eaf2 Provide originalNotesCount and originalUsersCount 2018-05-08 07:19:23 +09:00
ecd71ef5ff Fix bug 2018-05-08 07:08:02 +09:00
058602352c Fix #1574 2018-05-08 07:03:06 +09:00
59c39fab13 Update setup.ja.md 2018-05-08 04:04:18 +09:00
ab4f97ff20 2.5.0 2018-05-07 18:22:44 +09:00
79640d6861 Fix bug 2018-05-07 18:20:15 +09:00
d305c7e401 Fix 2018-05-07 17:45:21 +09:00
8afaca36d9 Better text parsing 2018-05-07 17:19:00 +09:00
9950fafff7 oops 2018-05-07 17:15:20 +09:00
0d9857db63 2.5.0-beta1 2018-05-07 17:05:17 +09:00
10c932876d Better banner redering 2018-05-07 17:05:05 +09:00
d5d7a527a0 Fix #1569 2018-05-07 16:53:56 +09:00
9eb8595130 Fix ui 2018-05-07 16:39:00 +09:00
8c46e5b3d9 投稿のレンダリングを改善 2018-05-07 16:31:33 +09:00
73bd877993 Merge branch 'master' of https://github.com/syuilo/misskey 2018-05-07 16:25:21 +09:00
de6cbf8a22 リモートアカウントの投稿に対しての返信にはメンションを付けるように 2018-05-07 16:25:06 +09:00
89f618d732 Update i18n.ts 2018-05-07 05:22:06 +09:00
4955df3911 oops 2018-05-07 04:26:45 +09:00
6785f50a1f 良い感じに 2018-05-07 04:08:39 +09:00
a549327170 Merge branch 'master' of https://github.com/syuilo/misskey 2018-05-07 03:19:27 +09:00
1f018d87f2 メンションを処理 2018-05-07 03:19:24 +09:00
769ee734fa Merge pull request #1572 from ha-dai/patch1
Fix typo.
2018-05-07 03:09:45 +09:00
6419185228 fix typo 2018-05-07 03:05:00 +09:00
b77cbeca22 #1427 2018-05-07 02:54:14 +09:00
4cce10a7d7 Fix glitch 2018-05-07 01:43:51 +09:00
1a6fd7d72e Fix ui glitch 2018-05-06 21:13:21 +09:00
97ce939a73 Fix 2018-05-06 18:19:17 +09:00
3f2a7a561e アイコンのレンダリングを改善 2018-05-06 18:04:37 +09:00
332af15e3c Fix 2018-05-06 17:37:50 +09:00
fe184ce84a Fix 2018-05-06 17:36:47 +09:00
661c7f45ba Merge pull request #1570 from m4sk1n/master
i18n: Improve Polish translation
2018-05-06 16:52:50 +09:00
01c0545409 i18n: Improve Polish translation
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-05-06 09:48:53 +02:00
c6492d3d58 誰もフォローしていないときはローカルタイムラインを表示するように 2018-05-06 14:35:28 +09:00
638a2ab684 2.3.1 2018-05-06 02:08:40 +09:00
650f79d0fd Fix bugs 2018-05-06 02:08:27 +09:00
a64817cea1 2.3.0 2018-05-06 01:47:25 +09:00
c32c3c1370 Fix bug 2018-05-06 01:46:35 +09:00
29ad7ab0cf Provide prev and next note link 2018-05-06 01:43:53 +09:00
11716fa9d3 Provide og:type 2018-05-06 01:37:32 +09:00
210f11ffd8 Update theme color 🎨 2018-05-06 01:35:17 +09:00
814751d76a Merge branch 'master' of https://github.com/syuilo/misskey 2018-05-06 01:34:51 +09:00
440cf139bb メタ情報をレンダリングするように 2018-05-06 01:34:48 +09:00
276d8ffc66 Update README.md 2018-05-05 02:46:23 +09:00
d9cdc1f079 2.2.0 2018-05-04 18:32:13 +09:00
414c600356 ファイルのURLを保存するように 2018-05-04 18:32:03 +09:00
e37c19fdcd 2.1.4 2018-05-04 18:02:17 +09:00
d69b919961 oops 2018-05-04 18:02:09 +09:00
1311db8060 2.1.3 2018-05-04 18:00:02 +09:00
ed9e7520f1 Fix bug 2018-05-04 17:59:51 +09:00
f850283147 Merge branch 'master' of github.com:syuilo/misskey 2018-05-04 02:49:46 +09:00
c512c07630 Merge branch 'master' of github.com:syuilo/misskey 2017-12-07 17:23:12 +09:00
105623e398 Merge branch 'master' of github.com:syuilo/misskey 2017-11-30 13:24:41 +09:00
6c75bc6d51 Merge branch 'master' of github.com:syuilo/misskey 2017-11-27 03:41:47 +09:00
a068741d05 Merge branch 'master' of https://github.com/syuilo/misskey 2017-11-06 13:37:48 +09:00
4e83106853 Merge branch 'master' of https://github.com/syuilo/misskey 2017-11-02 13:58:47 +09:00
0b688a909e Merge branch 'master' of https://github.com/syuilo/misskey 2017-10-26 15:05:20 +09:00
fabcad6db9 Merge branch 'master' of https://github.com/syuilo/misskey 2017-10-25 16:24:16 +09:00
48 changed files with 699 additions and 380 deletions

View File

@ -52,9 +52,9 @@ If you want to translate Misskey, please see [Translation guide](./docs/translat
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors) [List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
### :earth_americas: Translators ### :earth_americas: Translators
| ![][mirro-san-icon] | ![][Conan-kun-icon] | | ![][mirro-san-icon] | ![][Conan-kun-icon] | ![][m4sk1n-icon] |
|:-:|:-:| |:-:|:-:|:-:|
| [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | | [Mirro][mirro-san-link]<br>English, French | [Asriel][Conan-kun-link]<br>English, French | [Marcin Mikołajczak][m4sk1n-link]<br>Polish |
:four_leaf_clover: Copyright :four_leaf_clover: Copyright
---------------------------------------------------------------- ----------------------------------------------------------------
@ -100,4 +100,5 @@ Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE).
[mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4 [mirro-san-icon]: https://avatars1.githubusercontent.com/u/17948612?s=70&v=4
[Conan-kun-link]: https://github.com/Conan-kun [Conan-kun-link]: https://github.com/Conan-kun
[Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4 [Conan-kun-icon]: https://avatars3.githubusercontent.com/u/30003708?s=70&v=4
[m4sk1n-link]: https://github.com/m4sk1n
[m4sk1n-icon]: https://avatars3.githubusercontent.com/u/21127288?s=70&v=4

View File

@ -67,3 +67,15 @@ web-push generate-vapid-keys
1. `git reset --hard && git pull origin master` 1. `git reset --hard && git pull origin master`
2. `npm install` 2. `npm install`
3. `npm run build` 3. `npm run build`
## メモリが足りなくてビルドできない場合
Misskeyの(クライアントの)ビルドには、目安として8GBくらいのメモリを必要とします。
VPSなどでビルドする時は、もしかしたらメモリが足りなくなる可能性があります。
そうなった場合、もしVPSではなくあなたのPCが十分なメモリを搭載しているなら、あなたのPC上でビルドし、生成されたファイルをVPSにFTPでアップロードする方法を採ることができます。
1. あなたのPC上にMisskeyをインストールする
2. 設定ファイルを用意する。設定ファイルは、サーバーに合わせた設定にします。
3. npm run webpack
4. built/client をサーバーにアップロードする
5. サーバー上で、npm run gulp
6. 完了

View File

@ -59,9 +59,15 @@ gulp.task('build:ts', () => {
.pipe(gulp.dest('./built/')); .pipe(gulp.dest('./built/'));
}); });
gulp.task('build:copy', () => gulp.task('build:copy:views', () =>
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
);
gulp.task('build:copy', ['build:copy:views'], () =>
gulp.src([ gulp.src([
'./build/Release/crypto_key.node', './build/Release/crypto_key.node',
'./src/const.json',
'./src/server/web/views/**/*',
'./src/**/assets/**/*', './src/**/assets/**/*',
'!./src/client/app/**/assets/**/*' '!./src/client/app/**/assets/**/*'
]).pipe(gulp.dest('./built/')) ]).pipe(gulp.dest('./built/'))

View File

@ -95,7 +95,7 @@ common/views/components/nav.vue:
donors: "Sponsorzy" donors: "Sponsorzy"
repository: "Repozytorium" repository: "Repozytorium"
develop: "Autorzy" develop: "Autorzy"
feedback: "Opinie" feedback: "Podziel się opinią"
common/views/components/note-menu.vue: common/views/components/note-menu.vue:
favorite: "Dodaj do ulubionych" favorite: "Dodaj do ulubionych"
@ -232,7 +232,7 @@ desktop/views/components/drive.nav-folder.vue:
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Szukaj" search: "Szukaj"
load-more: "Załaduj więcej" load-more: "Załaduj więcej"
empty-drive: "Twój dysk jest posty" empty-drive: "Twój dysk jest pusty"
empty-drive-description: "Możesz wysłać plik klikając prawym przyciskiem myszy i wybierając \"Wyślij plik\" lub przeciągnąć plik i upuścić w tym oknie." empty-drive-description: "Możesz wysłać plik klikając prawym przyciskiem myszy i wybierając \"Wyślij plik\" lub przeciągnąć plik i upuścić w tym oknie."
empty-folder: "Ten katalog jest posty" empty-folder: "Ten katalog jest posty"
unable-to-process: "Nie udało się dokończyć działania." unable-to-process: "Nie udało się dokończyć działania."
@ -240,6 +240,7 @@ desktop/views/components/drive.vue:
unhandled-error: "Nieznany błąd" unhandled-error: "Nieznany błąd"
url-upload: "Wyślij z adresu" url-upload: "Wyślij z adresu"
url-of-file: "Adres URL pliku, który chcesz wysłać" url-of-file: "Adres URL pliku, który chcesz wysłać"
url-upload-requested: "Zaplanowano wysyłanie"
may-take-time: "Może trochę potrwać, zanim wysyłanie zostanie ukończone." may-take-time: "Może trochę potrwać, zanim wysyłanie zostanie ukończone."
create-folder: "Utwórz katalog" create-folder: "Utwórz katalog"
folder-name: "Nazwa katalogu" folder-name: "Nazwa katalogu"
@ -254,6 +255,7 @@ desktop/views/components/messaging-window.vue:
desktop/views/components/notes.note.vue: desktop/views/components/notes.note.vue:
reposted-by: "Udostępniono przez {}" reposted-by: "Udostępniono przez {}"
reply: "Odpowiedz" reply: "Odpowiedz"
renote: "Przeredaguj"
add-reaction: "Dodaj reakcję" add-reaction: "Dodaj reakcję"
detail: "Pokaż szczegóły" detail: "Pokaż szczegóły"
@ -267,11 +269,13 @@ desktop/views/components/post-form.vue:
quote-placeholder: "Zacytuj ten wpis…" quote-placeholder: "Zacytuj ten wpis…"
note: "Wyślij" note: "Wyślij"
reply: "Odpowiedz" reply: "Odpowiedz"
posted: "Posted!" renote: "Przeredaguj"
posted: "Opublikowano!"
replied: "Odpowiedziano!" replied: "Odpowiedziano!"
reposted: "Udostępniono!" reposted: "Udostępniono!"
note-failed: "Nie udało się wysłać" note-failed: "Nie udało się wysłać"
reply-failed: "Nie udało się odpowiedzieć" reply-failed: "Nie udało się odpowiedzieć"
renote-failed: "Nie udało się przeredagować"
posting: "Wysyłanie" posting: "Wysyłanie"
attach-media-from-local: "Załącz zawartość multimedialną z komputera" attach-media-from-local: "Załącz zawartość multimedialną z komputera"
attach-media-from-drive: "Załącz zawartość multimedialną z dysku" attach-media-from-drive: "Załącz zawartość multimedialną z dysku"
@ -289,14 +293,19 @@ desktop/views/components/post-form-window.vue:
desktop/views/components/renote-form.vue: desktop/views/components/renote-form.vue:
quote: "Cytuj…" quote: "Cytuj…"
cancel: "Anuluj" cancel: "Anuluj"
renote: "Przeredaguj"
reposting: "Udostępnianie…" reposting: "Udostępnianie…"
success: "Udostępniono!" success: "Udostępniono!"
failure: "Nie udało się przeredagować"
desktop/views/components/renote-form-window.vue:
title: "Czy na pewno chcesz przeredagować ten wpis?"
desktop/views/components/settings.vue: desktop/views/components/settings.vue:
profile: "Profil" profile: "Profil"
notification: "Powiadomienie" notification: "Powiadomienia"
apps: "Aplikacje" apps: "Aplikacje"
mute: "Wycisz" mute: "Wyciszanie"
drive: "Dysk" drive: "Dysk"
security: "Bezpieczeństwo" security: "Bezpieczeństwo"
password: "Hasło" password: "Hasło"
@ -392,7 +401,7 @@ desktop/views/pages/user/user.followers-you-know.vue:
no-users: "Brak użytkowników" no-users: "Brak użytkowników"
desktop/views/pages/user/user.friends.vue: desktop/views/pages/user/user.friends.vue:
title: "Najczęściej odpisujący" title: "Najbardziej aktywni"
loading: "Ładowanie" loading: "Ładowanie"
no-users: "Brak użytkowników" no-users: "Brak użytkowników"

71
migration/2.4.0.js Normal file
View File

@ -0,0 +1,71 @@
// for Node.js interpret
const chalk = require('chalk');
const sequential = require('promise-sequential');
const { default: User } = require('../built/models/user');
const { default: DriveFile } = require('../built/models/drive-file');
async function main() {
const promiseGens = [];
const count = await User.count({});
let prev;
for (let i = 0; i < count; i++) {
promiseGens.push(() => {
const promise = new Promise(async (res, rej) => {
const user = await User.findOne(prev ? {
_id: { $gt: prev._id }
} : {}, {
sort: {
_id: 1
}
});
prev = user;
const set = {};
if (user.avatarId != null) {
const file = await DriveFile.findOne({ _id: user.avatarId });
if (file && file.metadata.properties.avgColor) {
set.avatarColor = file.metadata.properties.avgColor;
}
}
if (user.bannerId != null) {
const file = await DriveFile.findOne({ _id: user.bannerId });
if (file && file.metadata.properties.avgColor) {
set.bannerColor = file.metadata.properties.avgColor;
}
}
if (Object.keys(set).length === 0) return res([i, user]);
User.update({
_id: user._id
}, {
$set: set
}).then(() => {
res([i, user]);
}).catch(rej);
});
promise.then(([i, user]) => {
console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`);
});
return promise;
});
}
return await sequential(promiseGens);
}
main().then(() => {
console.log('ALL DONE');
}).catch(console.error);

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "2.1.2", "version": "2.6.2",
"clientVersion": "1.0.5193", "clientVersion": "1.0.5260",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -58,6 +58,7 @@
"@types/koa-multer": "1.0.0", "@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.28", "@types/koa-router": "7.0.28",
"@types/koa-send": "4.1.1", "@types/koa-send": "4.1.1",
"@types/koa-views": "^2.0.3",
"@types/koa__cors": "2.2.2", "@types/koa__cors": "2.2.2",
"@types/kue": "0.11.8", "@types/kue": "0.11.8",
"@types/license-checker": "15.0.0", "@types/license-checker": "15.0.0",
@ -68,6 +69,7 @@
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "9.6.6", "@types/node": "9.6.6",
"@types/nopt": "3.0.29", "@types/nopt": "3.0.29",
"@types/parse5": "^3.0.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "0.8.1", "@types/qrcode": "0.8.1",
"@types/ratelimiter": "2.1.28", "@types/ratelimiter": "2.1.28",
@ -146,6 +148,7 @@
"koa-router": "7.4.0", "koa-router": "7.4.0",
"koa-send": "4.1.3", "koa-send": "4.1.3",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "^6.1.4",
"kue": "0.11.6", "kue": "0.11.6",
"license-checker": "18.0.0", "license-checker": "18.0.0",
"loader-utils": "1.1.0", "loader-utils": "1.1.0",
@ -164,6 +167,7 @@
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0", "on-build-webpack": "0.1.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "^4.0.0",
"progress-bar-webpack-plugin": "1.11.0", "progress-bar-webpack-plugin": "1.11.0",
"prominence": "0.2.0", "prominence": "0.2.0",
"promise-sequential": "^1.1.1", "promise-sequential": "^1.1.1",

View File

@ -7,7 +7,7 @@ import locale from '../../locales';
export default class Replacer { export default class Replacer {
private lang: string; private lang: string;
public pattern = /%i18n:([a-z_\-@\.\!]+?)%/g; public pattern = /%i18n:([a-z0-9_\-@\.\!]+?)%/g;
constructor(lang: string) { constructor(lang: string) {
this.lang = lang; this.lang = lang;

View File

@ -1,3 +1,5 @@
block vars
doctype html doctype html
!= '\n<!-- Thank you for using Misskey! @syuilo -->\n' != '\n<!-- Thank you for using Misskey! @syuilo -->\n'
@ -9,9 +11,17 @@ html
meta(name='application-name' content='Misskey') meta(name='application-name' content='Misskey')
meta(name='theme-color' content=themeColor) meta(name='theme-color' content=themeColor)
meta(name='referrer' content='origin') meta(name='referrer' content='origin')
meta(property='og:site_name' content='Misskey')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
title Misskey title
block title
| Misskey
block desc
meta(name='description' content='A SNS')
block meta
style style
include ./../../../built/client/assets/init.css include ./../../../built/client/assets/init.css

View File

@ -1,8 +1,6 @@
<template> <template>
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="{ borderRadius: clientSettings.circleIcons ? '100%' : null }"> <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-if="disablePreview"></router-link>
<img v-if="disablePreview" :src="`${user.avatarUrl}?thumbnail&size=128`" alt=""/> <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else v-user-preview="user.id"></router-link>
<img v-else :src="`${user.avatarUrl}?thumbnail&size=128`" alt="" v-user-preview="user.id"/>
</router-link>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -10,6 +8,7 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: { props: {
user: { user: {
type: Object,
required: true required: true
}, },
target: { target: {
@ -20,6 +19,15 @@ export default Vue.extend({
required: false, required: false,
default: false default: false
} }
},
computed: {
style(): any {
return {
backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
};
}
} }
}); });
</script> </script>
@ -27,13 +35,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-avatar .mk-avatar
display inline-block display inline-block
> img
display inline-block
width 100%
height 100%
margin 0
border-radius inherit
vertical-align bottom vertical-align bottom
background-size cover
background-position center center
transition border-radius 1s ease transition border-radius 1s ease
</style> </style>

View File

@ -45,7 +45,7 @@ export default Vue.extend({
} else if (url.hostname == 'youtu.be') { } else if (url.hostname == 'youtu.be') {
this.youtubeId = url.pathname; this.youtubeId = url.pathname;
} else { } else {
fetch('/url?url=' + this.url).then(res => { fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
res.json().then(info => { res.json().then(info => {
this.title = info.title; this.title = info.title;
this.description = info.description; this.description = info.description;

View File

@ -333,7 +333,7 @@ root(isDark)
> .renote > .renote
display flex display flex
align-items baseline align-items center
padding 16px 32px padding 16px 32px
line-height 28px line-height 28px
color #9dbb00 color #9dbb00

View File

@ -105,21 +105,11 @@ export default Vue.extend({
} }
}, },
watch: {
text() {
this.saveDraft();
},
poll() {
this.saveDraft();
},
files() {
this.saveDraft();
}
},
mounted() { mounted() {
if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
}
this.$nextTick(() => { this.$nextTick(() => {
// 書きかけの投稿を復元 // 書きかけの投稿を復元
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId]; const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
@ -134,10 +124,18 @@ export default Vue.extend({
} }
this.$emit('change-attached-media', this.files); this.$emit('change-attached-media', this.files);
} }
this.$nextTick(() => this.watch());
}); });
}, },
methods: { methods: {
watch() {
this.$watch('text', () => this.saveDraft());
this.$watch('poll', () => this.saveDraft());
this.$watch('files', () => this.saveDraft());
},
focus() { focus() {
(this.$refs.text as any).focus(); (this.$refs.text as any).focus();
}, },

View File

@ -31,6 +31,12 @@ export default Vue.extend({
}; };
}, },
created() {
if ((this as any).os.i.followingCount == 0) {
this.src = 'local';
}
},
mounted() { mounted() {
(this.$refs.tl as any).$once('loaded', () => { (this.$refs.tl as any).$once('loaded', () => {
this.$emit('loaded'); this.$emit('loaded');

View File

@ -62,7 +62,7 @@ export default Vue.extend({
more() { more() {
this.moreFetching = true; this.moreFetching = true;
(this as any).api('notes/list-timeline', { (this as any).api('notes/user-list-timeline', {
listId: this.list.id, listId: this.list.id,
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id,

View File

@ -1,9 +1,11 @@
<template> <template>
<mk-ui> <mk-ui>
<main v-if="!fetching"> <main v-if="!fetching">
<a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:@next%</a>
<mk-note-detail :note="note"/> <mk-note-detail :note="note"/>
<a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:@prev%</a> <footer>
<router-link v-if="note.next" :to="note.next">%fa:angle-left% %i18n:@next%</router-link>
<router-link v-if="note.prev" :to="note.prev">%i18n:@prev% %fa:angle-right%</router-link>
</footer>
</main> </main>
</mk-ui> </mk-ui>
</template> </template>
@ -48,17 +50,12 @@ main
padding 16px padding 16px
text-align center text-align center
> footer
margin-top 16px
> a > a
display inline-block display inline-block
margin 0 16px
&:first-child
margin-bottom 4px
&:last-child
margin-top 4px
> [data-fa]
margin-right 4px
> .mk-note-detail > .mk-note-detail
margin 0 auto margin 0 auto

View File

@ -2,8 +2,8 @@
<div class="header" :data-is-dark-background="user.bannerUrl != null"> <div class="header" :data-is-dark-background="user.bannerUrl != null">
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div> <div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> <div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''" @click="onBannerClick"></div> <div class="banner" ref="banner" :style="style" @click="onBannerClick"></div>
<div class="fade"></div> <div class="fade"></div>
</div> </div>
<div class="container"> <div class="container">
@ -25,6 +25,15 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['user'], props: ['user'],
computed: {
style(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
}
},
mounted() { mounted() {
if (this.user.bannerUrl) { if (this.user.bannerUrl) {
window.addEventListener('load', this.onScroll); window.addEventListener('load', this.onScroll);

View File

@ -252,7 +252,7 @@ root(isDark)
> .renote > .renote
display flex display flex
align-items baseline align-items center
padding 8px 16px padding 8px 16px
line-height 28px line-height 28px
color #9dbb00 color #9dbb00

View File

@ -188,6 +188,11 @@ export default Vue.extend({
} }
if ((this as any).clientSettings.fetchOnScroll !== false) { if ((this as any).clientSettings.fetchOnScroll !== false) {
// 親要素が display none だったら弾く
// https://github.com/syuilo/misskey/issues/1569
// http://d.hatena.ne.jp/favril/20091105/1257403319
if (this.$el.offsetHeight == 0) return;
const current = window.scrollY + window.innerHeight; const current = window.scrollY + window.innerHeight;
if (current > document.body.offsetHeight - 8) this.loadMore(); if (current > document.body.offsetHeight - 8) this.loadMore();
} }

View File

@ -25,6 +25,7 @@
</div> </div>
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/> <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
<footer>
<button class="upload" @click="chooseFile">%fa:upload%</button> <button class="upload" @click="chooseFile">%fa:upload%</button>
<button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button> <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" @click="kao">%fa:R smile%</button> <button class="kao" @click="kao">%fa:R smile%</button>
@ -32,6 +33,7 @@
<button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button> <button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button> <button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
</footer>
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/> <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
</div> </div>
</div> </div>
@ -67,6 +69,10 @@ export default Vue.extend({
}, },
mounted() { mounted() {
if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
}
this.$nextTick(() => { this.$nextTick(() => {
this.focus(); this.focus();
}); });
@ -332,12 +338,13 @@ root(isDark)
> .file > .file
display none display none
> .upload > footer
> .drive white-space nowrap
> .kao overflow auto
> .poll -webkit-overflow-scrolling touch
> .geo overflow-scrolling touch
> .visibility
> *
display inline-block display inline-block
padding 0 padding 0
margin 0 margin 0

View File

@ -62,7 +62,7 @@ export default Vue.extend({
more() { more() {
this.moreFetching = true; this.moreFetching = true;
(this as any).api('notes/list-timeline', { (this as any).api('notes/user-list-timeline', {
listId: this.list.id, listId: this.list.id,
limit: fetchLimit + 1, limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id, untilId: (this.$refs.timeline as any).tail().id,

View File

@ -75,6 +75,12 @@ export default Vue.extend({
} }
}, },
created() {
if ((this as any).os.i.followingCount == 0) {
this.src = 'local';
}
},
mounted() { mounted() {
document.title = 'Misskey'; document.title = 'Misskey';

View File

@ -6,8 +6,8 @@
<mk-note-detail :note="note"/> <mk-note-detail :note="note"/>
</div> </div>
<footer> <footer>
<a v-if="note.prev" :href="note.prev">%fa:angle-left% %i18n:@prev%</a> <router-link v-if="note.prev" :to="note.prev">%fa:angle-left% %i18n:@prev%</router-link>
<a v-if="note.next" :href="note.next">%i18n:@next% %fa:angle-right%</a> <router-link v-if="note.next" :to="note.next">%i18n:@next% %fa:angle-right%</router-link>
</footer> </footer>
</main> </main>
</mk-ui> </mk-ui>

View File

@ -5,11 +5,11 @@
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div> <div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div> <div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
<header> <header>
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> <div class="banner" :style="style"></div>
<div class="body"> <div class="body">
<div class="top"> <div class="top">
<a class="avatar"> <a class="avatar">
<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> <img :src="user.avatarUrl" alt="avatar"/>
</a> </a>
<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
</div> </div>
@ -80,6 +80,13 @@ export default Vue.extend({
computed: { computed: {
age(): number { age(): number {
return age(this.user.profile.birthday); return age(this.user.profile.birthday);
},
style(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundImage: `url(${ this.user.bannerUrl })`
};
} }
}, },
watch: { watch: {

View File

@ -14,13 +14,13 @@ mixin propTable(props)
if prop.kind == 'id' if prop.kind == 'id'
if prop.entity if prop.entity
| ( | (
a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
| ID) | ID)
else else
| (ID) | (ID)
else if prop.kind == 'entity' else if prop.kind == 'entity'
| ( | (
a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
| ) | )
else if prop.kind == 'object' else if prop.kind == 'object'
if prop.def if prop.def

View File

@ -1,5 +1,5 @@
{ {
"copyright": "Copyright (c) 2014-2018 syuilo", "copyright": "Copyright (c) 2014-2018 syuilo",
"themeColor": "#5cbb2d", "themeColor": "#f66e4f",
"themeColorForeground": "#fff" "themeColorForeground": "#fff"
} }

View File

@ -9,6 +9,7 @@ import User from './user';
import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumbnail'; import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumbnail';
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
DriveFile.createIndex('md5');
DriveFile.createIndex('metadata.uri', { sparse: true, unique: true }); DriveFile.createIndex('metadata.uri', { sparse: true, unique: true });
export default DriveFile; export default DriveFile;
@ -28,7 +29,8 @@ export type IMetadata = {
_user: any; _user: any;
folderId: mongo.ObjectID; folderId: mongo.ObjectID;
comment: string; comment: string;
uri: string; uri?: string;
url?: string;
deletedAt?: Date; deletedAt?: Date;
isExpired?: boolean; isExpired?: boolean;
}; };

View File

@ -15,9 +15,8 @@ import Notification, { deleteNotification } from './notification';
import Following from './following'; import Following from './following';
const Note = db.get<INote>('notes'); const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true }); Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
export default Note; export default Note;
export function isValidText(text: string): boolean { export function isValidText(text: string): boolean {
@ -271,41 +270,10 @@ export const pack = async (
// When requested a detailed note data // When requested a detailed note data
if (opts.detail) { if (opts.detail) {
// Get previous note info //#region 重いので廃止
_note.prev = (async () => { _note.prev = null;
const prev = await Note.findOne({ _note.next = null;
userId: _note.userId, //#endregion
_id: {
$lt: id
}
}, {
fields: {
_id: true
},
sort: {
_id: -1
}
});
return prev ? prev._id : null;
})();
// Get next note info
_note.next = (async () => {
const next = await Note.findOne({
userId: _note.userId,
_id: {
$gt: id
}
}, {
fields: {
_id: true
},
sort: {
_id: 1
}
});
return next ? next._id : null;
})();
if (_note.replyId) { if (_note.replyId) {
// Populate reply to note // Populate reply to note

View File

@ -2,6 +2,7 @@ import { createQueue } from 'kue';
import config from '../config'; import config from '../config';
import http from './processors/http'; import http from './processors/http';
import { ILocalUser } from '../models/user';
const queue = createQueue({ const queue = createQueue({
redis: { redis: {
@ -20,7 +21,7 @@ export function createHttp(data) {
.backoff({ delay: 16384, type: 'exponential' }); .backoff({ delay: 16384, type: 'exponential' });
} }
export function deliver(user, content, to) { export function deliver(user: ILocalUser, content, to) {
createHttp({ createHttp({
title: 'deliver', title: 'deliver',
type: 'deliver', type: 'deliver',

View File

@ -7,13 +7,14 @@ export default async (job: kue.Job, done): Promise<void> => {
await request(job.data.user, job.data.to, job.data.content); await request(job.data.user, job.data.to, job.data.content);
done(); done();
} catch (res) { } catch (res) {
if (res.statusCode == null) return done();
if (res.statusCode >= 400 && res.statusCode < 500) { if (res.statusCode >= 400 && res.statusCode < 500) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく // 何回再送しても成功することはないということなのでエラーにはしないでおく
done(); done();
} else { } else {
console.warn(`deliver failed: ${res.statusMessage}`); console.warn(`deliver failed: ${res.statusMessage}`);
done(new Error(res.statusMessage)); done(res);
} }
} }
}; };

View File

@ -11,7 +11,12 @@ export default function(note: INote) {
if (note.poll != null) { if (note.poll != null) {
const url = `${config.url}/notes/${note._id}`; const url = `${config.url}/notes/${note._id}`;
// TODO: i18n // TODO: i18n
html += `<p>【投票】<br />${url}</p>`; html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
}
if (note.renoteId != null) {
const url = `${config.url}/notes/${note.renoteId}`;
html += `<p>RE: <a href="${url}">${url}</a></p>`;
} }
return html; return html;

View File

@ -24,7 +24,7 @@ export async function createImage(actor: IRemoteUser, value): Promise<IDriveFile
log(`Creating the Image: ${image.url}`); log(`Creating the Image: ${image.url}`);
return await uploadFromUrl(image.url, actor); return await uploadFromUrl(image.url, actor, null, image.url);
} }
/** /**

View File

@ -1,5 +1,5 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { JSDOM } from 'jsdom'; import * as parse5 from 'parse5';
import * as debug from 'debug'; import * as debug from 'debug';
import config from '../../../config'; import config from '../../../config';
@ -13,6 +13,76 @@ import { IRemoteUser } from '../../../models/user';
const log = debug('misskey:activitypub'); const log = debug('misskey:activitypub');
function parse(html: string): string {
const dom = parse5.parseFragment(html) as parse5.AST.Default.Document;
let text = '';
dom.childNodes.forEach(n => analyze(n));
return text.trim();
function getText(node) {
if (node.nodeName == '#text') return node.value;
if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join('');
}
return '';
}
function analyze(node) {
switch (node.nodeName) {
case '#text':
text += node.value;
break;
case 'br':
text += '\n';
break;
case 'a':
const txt = getText(node);
// メンション
if (txt.startsWith('@')) {
const part = txt.split('@');
if (part.length == 2) {
//#region ホスト名部分が省略されているので復元する
const href = new URL(node.attrs.find(x => x.name == 'href').value);
const acct = txt + '@' + href.hostname;
text += acct;
break;
//#endregion
} else if (part.length == 3) {
text += txt;
break;
}
}
if (node.childNodes) {
node.childNodes.forEach(n => analyze(n));
}
break;
case 'p':
text += '\n\n';
if (node.childNodes) {
node.childNodes.forEach(n => analyze(n));
}
break;
default:
if (node.childNodes) {
node.childNodes.forEach(n => analyze(n));
}
break;
}
}
}
/** /**
* Noteをフェッチします。 * Noteをフェッチします。
* *
@ -87,9 +157,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// リプライ // リプライ
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
// MastodonはHTMLを送り付けてくる // テキストのパース
// そして改行は<br />で表現されている const text = parse(note.content);
const { window } = new JSDOM(note.content.replace(/<br \/>/g, '\n'));
// ユーザーの情報が古かったらついでに更新しておく // ユーザーの情報が古かったらついでに更新しておく
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
@ -101,7 +170,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
media, media,
reply, reply,
renote: undefined, renote: undefined,
text: window.document.body.textContent, text: text,
viaMobile: false, viaMobile: false,
geo: undefined, geo: undefined,
visibility, visibility,

View File

@ -15,6 +15,7 @@ export interface IObject {
icon?: any; icon?: any;
image?: any; image?: any;
url?: string; url?: string;
tag?: any[];
} }
export interface IActivity extends IObject { export interface IActivity extends IObject {

View File

@ -1,11 +1,16 @@
import { toUnicode, toASCII } from 'punycode'; import { toUnicode, toASCII } from 'punycode';
import User from '../models/user'; import User, { IUser } from '../models/user';
import webFinger from './webfinger'; import webFinger from './webfinger';
import config from '../config'; import config from '../config';
import { createPerson } from './activitypub/models/person'; import { createPerson } from './activitypub/models/person';
export default async (username, _host, option) => { export default async (username, _host, option?): Promise<IUser> => {
const usernameLower = username.toLowerCase(); const usernameLower = username.toLowerCase();
if (_host == null) {
return await User.findOne({ usernameLower });
}
const hostAscii = toASCII(_host).toLowerCase(); const hostAscii = toASCII(_host).toLowerCase();
const host = toUnicode(hostAscii); const host = toUnicode(hostAscii);

View File

@ -1,6 +1,6 @@
/** /**
* 投稿を表す文字列を取得します。 * 投稿を表す文字列を取得します。
* @param {*} note 投稿 * @param {*} note (packされた)投稿
*/ */
const summarize = (note: any): string => { const summarize = (note: any): string => {
if (note.isHidden) { if (note.isHidden) {

View File

@ -57,6 +57,8 @@ export default (
.count({ .count({
recipientId: userId, recipientId: userId,
isRead: false isRead: false
}, {
limit: 1
}); });
if (count == 0) { if (count == 0) {

View File

@ -43,6 +43,8 @@ export default (
.count({ .count({
notifieeId: userId, notifieeId: userId,
isRead: false isRead: false
}, {
limit: 1
}); });
if (count == 0) { if (count == 0) {

View File

@ -4,6 +4,7 @@
import $ from 'cafy'; import ID from '../../../../cafy-id'; import $ from 'cafy'; import ID from '../../../../cafy-id';
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
import event from '../../../../publishers/stream'; import event from '../../../../publishers/stream';
import DriveFile from '../../../../models/drive-file';
/** /**
* Update myself * Update myself
@ -51,12 +52,34 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
if (autoWatchErr) return rej('invalid autoWatch param'); if (autoWatchErr) return rej('invalid autoWatch param');
if (autoWatch != null) user.settings.autoWatch = autoWatch; if (autoWatch != null) user.settings.autoWatch = autoWatch;
if (avatarId) {
const avatar = await DriveFile.findOne({
_id: avatarId
});
if (avatar != null && avatar.metadata.properties.avgColor) {
user.avatarColor = avatar.metadata.properties.avgColor;
}
}
if (bannerId) {
const banner = await DriveFile.findOne({
_id: bannerId
});
if (banner != null && banner.metadata.properties.avgColor) {
user.bannerColor = banner.metadata.properties.avgColor;
}
}
await User.update(user._id, { await User.update(user._id, {
$set: { $set: {
name: user.name, name: user.name,
description: user.description, description: user.description,
avatarId: user.avatarId, avatarId: user.avatarId,
avatarColor: user.avatarColor,
bannerId: user.bannerId, bannerId: user.bannerId,
bannerColor: user.bannerColor,
profile: user.profile, profile: user.profile,
isBot: user.isBot, isBot: user.isBot,
settings: user.settings settings: user.settings

View File

@ -38,12 +38,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
if (pollErr) return rej('invalid poll param'); if (pollErr) return rej('invalid poll param');
const query = { const query = {
createdAt: { _id: { $gte: new Date(Date.now() - ms('1days')) },
$gte: new Date(Date.now() - ms('1days')) renoteCount: { $gt: 0 },
}, '_user.host': null
renoteCount: {
$gt: 0
}
} as any; } as any;
if (reply != undefined) { if (reply != undefined) {

View File

@ -1,48 +1,26 @@
/**
* Module dependencies
*/
import Note from '../../../models/note'; import Note from '../../../models/note';
import User from '../../../models/user'; import User from '../../../models/user';
/** /**
* @swagger * Get the misskey's statistics
* /stats:
* note:
* summary: Show the misskey's statistics
* responses:
* 200:
* description: Success
* schema:
* type: object
* properties:
* notesCount:
* description: count of all notes of misskey
* type: number
* usersCount:
* description: count of all users of misskey
* type: number
*
* default:
* description: Failed
* schema:
* $ref: "#/definitions/Error"
*/
/**
* Show the misskey's statistics
*
* @param {any} params
* @return {Promise<any>}
*/ */
module.exports = params => new Promise(async (res, rej) => { module.exports = params => new Promise(async (res, rej) => {
const notesCount = await Note const notesCount = await Note.count();
.count();
const usersCount = await User const usersCount = await User.count();
.count();
const originalNotesCount = await Note.count({
'_user.host': null
});
const originalUsersCount = await User.count({
host: null
});
res({ res({
notesCount: notesCount, notesCount,
usersCount: usersCount usersCount,
originalNotesCount,
originalUsersCount
}); });
}); });

View File

@ -6,6 +6,8 @@ import * as mongodb from 'mongodb';
import DriveFile, { getDriveFileBucket } from '../../models/drive-file'; import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
const assets = `${__dirname}/../../server/file/assets/`;
const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
console.error(e); console.error(e);
ctx.status = 500; ctx.status = 500;
@ -25,16 +27,16 @@ export default async function(ctx: Koa.Context) {
if (file == null) { if (file == null) {
ctx.status = 404; ctx.status = 404;
await send(ctx, `${__dirname}/assets/dummy.png`); await send(ctx, '/dummy.png', { root: assets });
return; return;
} }
if (file.metadata.deletedAt) { if (file.metadata.deletedAt) {
ctx.status = 410; ctx.status = 410;
if (file.metadata.isExpired) { if (file.metadata.isExpired) {
await send(ctx, `${__dirname}/assets/cache-expired.png`); await send(ctx, '/cache-expired.png', { root: assets });
} else { } else {
await send(ctx, `${__dirname}/assets/tombstone.png`); await send(ctx, '/tombstone.png', { root: assets });
} }
return; return;
} }

View File

@ -7,14 +7,32 @@ import * as Koa from 'koa';
import * as Router from 'koa-router'; import * as Router from 'koa-router';
import * as send from 'koa-send'; import * as send from 'koa-send';
import * as favicon from 'koa-favicon'; import * as favicon from 'koa-favicon';
import * as views from 'koa-views';
import docs from './docs'; import docs from './docs';
import User from '../../models/user';
import parseAcct from '../../acct/parse';
import { fa } from '../../build/fa';
import config from '../../config';
import Note, { pack as packNote } from '../../models/note';
import getNoteSummary from '../../renderers/get-note-summary';
const consts = require('../../const.json');
const client = `${__dirname}/../../client/`; const client = `${__dirname}/../../client/`;
// Init app // Init app
const app = new Koa(); const app = new Koa();
// Init renderer
app.use(views(__dirname + '/views', {
extension: 'pug',
options: {
config,
themeColor: consts.themeColor,
facss: fa.dom.css()
}
}));
// Serve favicon // Serve favicon
app.use(favicon(`${client}/assets/favicon.ico`)); app.use(favicon(`${client}/assets/favicon.ico`));
@ -42,17 +60,21 @@ router.get('/assets/*', async ctx => {
// Apple touch icon // Apple touch icon
router.get('/apple-touch-icon.png', async ctx => { router.get('/apple-touch-icon.png', async ctx => {
await send(ctx, `${client}/assets/apple-touch-icon.png`); await send(ctx, '/assets/apple-touch-icon.png', {
root: client
});
}); });
// ServiceWroker // ServiceWroker
router.get(/^\/sw\.(.+?)\.js$/, async ctx => { //router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`); // await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`);
}); //});
// Manifest // Manifest
router.get('/manifest.json', async ctx => { router.get('/manifest.json', async ctx => {
await send(ctx, `${client}/assets/manifest.json`); await send(ctx, '/assets/manifest.json', {
root: client
});
}); });
//#endregion //#endregion
@ -63,6 +85,39 @@ router.use('/docs', docs.routes());
// URL preview endpoint // URL preview endpoint
router.get('/url', require('./url-preview')); router.get('/url', require('./url-preview'));
//#region for crawlers
// User
router.get('/@:user', async (ctx, next) => {
const { username, host } = parseAcct(ctx.params.user);
const user = await User.findOne({
usernameLower: username.toLowerCase(),
host
});
if (user != null) {
await ctx.render('user', { user });
} else {
// リモートユーザーなので
await next();
}
});
// Note
router.get('/notes/:note', async ctx => {
const note = await Note.findOne({ _id: ctx.params.note });
if (note != null) {
const _note = await packNote(note);
await ctx.render('note', {
note: _note,
summary: getNoteSummary(_note)
});
} else {
ctx.status = 404;
}
});
//#endregion
// Render base html for all requests // Render base html for all requests
router.get('*', async ctx => { router.get('*', async ctx => {
await send(ctx, `app/base.html`, { await send(ctx, `app/base.html`, {

View File

@ -14,6 +14,8 @@ module.exports = async (ctx: Koa.Context) => {
function wrap(url: string): string { function wrap(url: string): string {
return url != null return url != null
? `https://images.weserv.nl/?url=${url.replace(/^https?:\/\//, '')}` ? url.startsWith('https://') || url.startsWith('data:')
? url
: `https://images.weserv.nl/?url=${encodeURIComponent(url.replace(/^http:\/\//, ''))}`
: null; : null;
} }

View File

@ -0,0 +1,25 @@
extends ../../../../src/client/app/base
block vars
- const user = note.user;
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = `${config.url}/notes/${note.id}`;
block title
= `${title} | Misskey`
block desc
meta(name='description' content= summary)
block meta
meta(name='twitter:card' content='summary')
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= summary)
meta(property='og:url' content= url)
meta(property='og:image' content= user.avatarUrl)
if note.prev
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
if note.next
link(rel='next' href=`${config.url}/notes/${note.next}`)

View File

@ -0,0 +1,20 @@
extends ../../../../src/client/app/base
block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = config.url + '/@' + (user.host ? `${user.username}@${user.host}` : user.username);
- const img = user.avatarId ? `${config.drive_url}/${user.avatarId}` : null;
block title
= `${title} | Misskey`
block desc
meta(name='description' content= user.description)
block meta
meta(name='twitter:card' content='summary')
meta(property='og:type' content='blog')
meta(property='og:title' content= title)
meta(property='og:description' content= user.description)
meta(property='og:url' content= url)
meta(property='og:image' content= img)

View File

@ -62,6 +62,7 @@ const addFile = async (
comment: string = null, comment: string = null,
folderId: mongodb.ObjectID = null, folderId: mongodb.ObjectID = null,
force: boolean = false, force: boolean = false,
url: string = null,
uri: string = null uri: string = null
): Promise<IDriveFile> => { ): Promise<IDriveFile> => {
log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`); log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
@ -117,7 +118,8 @@ const addFile = async (
// Check if there is a file with the same hash // Check if there is a file with the same hash
const much = await DriveFile.findOne({ const much = await DriveFile.findOne({
md5: hash, md5: hash,
'metadata.userId': user._id 'metadata.userId': user._id,
'metadata.deletedAt': { $exists: false }
}); });
if (much !== null) { if (much !== null) {
@ -296,6 +298,10 @@ const addFile = async (
properties: properties properties: properties
} as IMetadata; } as IMetadata;
if (url !== null) {
metadata.url = url;
}
if (uri !== null) { if (uri !== null) {
metadata.uri = uri; metadata.uri = uri;
} }

View File

@ -43,7 +43,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
let error; let error;
try { try {
driveFile = await create(user, path, name, null, folderId, false, uri); driveFile = await create(user, path, name, null, folderId, false, url, uri);
log(`created: ${driveFile._id}`); log(`created: ${driveFile._id}`);
} catch (e) { } catch (e) {
error = e; error = e;

View File

@ -1,5 +1,5 @@
import Note, { pack, INote } from '../../models/note'; import Note, { pack, INote } from '../../models/note';
import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user'; import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream'; import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream';
import Following from '../../models/following'; import Following from '../../models/following';
import { deliver } from '../../queue'; import { deliver } from '../../queue';
@ -17,6 +17,62 @@ import event from '../../publishers/stream';
import parse from '../../text/parse'; import parse from '../../text/parse';
import { IApp } from '../../models/app'; import { IApp } from '../../models/app';
import UserList from '../../models/user-list'; import UserList from '../../models/user-list';
import resolveUser from '../../remote/resolve-user';
type Reason = 'reply' | 'quote' | 'mention';
/**
* ServiceWorkerへの通知を担当
*/
class NotificationManager {
private user: IUser;
private note: any;
private list: Array<{
user: ILocalUser['_id'],
reason: Reason;
}> = [];
constructor(user, note) {
this.user = user;
this.note = note;
}
public push(user: ILocalUser['_id'], reason: Reason) {
// 自分自身へは通知しない
if (this.user._id.equals(user)) return;
const exist = this.list.find(x => x.user.equals(user));
if (exist) {
// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
if (reason != 'mention') {
exist.reason = reason;
}
} else {
this.list.push({
user, reason
});
}
}
public deliver() {
this.list.forEach(async x => {
const mentionee = x.user;
// ミュート情報を取得
const mentioneeMutes = await Mute.find({
muterId: mentionee
});
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.user._id.toString())) {
pushSw(mentionee, x.reason, this.note);
}
});
}
}
export default async (user: IUser, data: { export default async (user: IUser, data: {
createdAt?: Date; createdAt?: Date;
@ -40,7 +96,7 @@ export default async (user: IUser, data: {
const tags = data.tags || []; const tags = data.tags || [];
let tokens = null; let tokens: any[] = null;
if (data.text) { if (data.text) {
// Analyze // Analyze
@ -119,8 +175,15 @@ export default async (user: IUser, data: {
// Serialize // Serialize
const noteObj = await pack(note); const noteObj = await pack(note);
// タイムラインへの投稿 const nm = new NotificationManager(user, noteObj);
if (note.channelId == null) {
const render = async () => {
const content = data.renote && data.text == null
? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
: renderCreate(await renderNote(note));
return packAp(content);
};
if (!silent) { if (!silent) {
if (isLocalUser(user)) { if (isLocalUser(user)) {
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') { if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
@ -190,12 +253,6 @@ export default async (user: IUser, data: {
} }
//#region リプライとAnnounceのAP配送 //#region リプライとAnnounceのAP配送
const render = async () => {
const content = data.renote && data.text == null
? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
: renderCreate(await renderNote(note));
return packAp(content);
};
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
@ -207,64 +264,53 @@ export default async (user: IUser, data: {
deliver(user, await render(), data.renote._user.inbox); deliver(user, await render(), data.renote._user.inbox);
} }
//#endergion //#endergion
//#region メンション
if (data.text) {
// TODO: Drop dupulicates
const mentions = tokens
.filter(t => t.type == 'mention');
let mentionedUsers = await Promise.all(mentions.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
} }
}));
// チャンネルへの投稿 // TODO: Drop dupulicates
/* TODO mentionedUsers = mentionedUsers.filter(x => x != null);
if (note.channelId) {
promises.push(
// Increment channel index(notes count)
Channel.update({ _id: note.channelId }, {
$inc: {
index: 1
}
}),
// Publish event to channel mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
promisedNoteObj.then(noteObj => { // 既に言及されたユーザーに対する返信や引用renoteの場合も無視
publishChannelStream(note.channelId, 'note', noteObj); if (data.reply && data.reply.userId.equals(u._id)) return;
}), if (data.renote && data.renote.userId.equals(u._id)) return;
Promise.all([ // Create notification
promisedNoteObj, notify(u._id, user._id, 'mention', {
noteId: note._id
// Get channel watchers
ChannelWatching.find({
channelId: note.channelId,
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
})
]).then(([noteObj, watches]) => {
// チャンネルの視聴者(のタイムライン)に配信
watches.forEach(w => {
stream(w.userId, 'note', noteObj);
}); });
})
);
}*/
const mentions = []; nm.push(u._id, 'mention');
});
async function addMention(mentionee, reason) {
// Reject if already added if (isLocalUser(user)) {
if (mentions.some(x => x.equals(mentionee))) return; mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
deliver(user, await render(), (u as IRemoteUser).inbox);
// Add mention });
mentions.push(mentionee); }
// Publish event // Append mentions data
if (!user._id.equals(mentionee)) { if (mentionedUsers.length > 0) {
const mentioneeMutes = await Mute.find({ Note.update({ _id: note._id }, {
muter_id: mentionee, $set: {
deleted_at: { $exists: false } mentions: mentionedUsers.map(u => u._id)
}
}); });
const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
event(mentionee, reason, noteObj);
pushSw(mentionee, reason, noteObj);
}
} }
} }
//#endregion
// If has in reply to note // If has in reply to note
if (data.reply) { if (data.reply) {
@ -303,8 +349,7 @@ export default async (user: IUser, data: {
watch(user._id, data.reply); watch(user._id, data.reply);
} }
// Add mention nm.push(data.reply.userId, 'reply');
addMention(data.reply.userId, 'reply');
} }
// If it is renote // If it is renote
@ -339,7 +384,7 @@ export default async (user: IUser, data: {
// If it is quote renote // If it is quote renote
if (data.text) { if (data.text) {
// Add mention // Add mention
addMention(data.renote.userId, 'quote'); nm.push(data.renote.userId, 'quote');
} else { } else {
// Publish event // Publish event
if (!user._id.equals(data.renote.userId)) { if (!user._id.equals(data.renote.userId)) {
@ -347,14 +392,17 @@ export default async (user: IUser, data: {
} }
} }
//#region TODO: これ重い
// 今までで同じ投稿をRenoteしているか // 今までで同じ投稿をRenoteしているか
const existRenote = await Note.findOne({ //const existRenote = await Note.findOne({
userId: user._id, // userId: user._id,
renoteId: data.renote._id, // renoteId: data.renote._id,
_id: { // _id: {
$ne: note._id // $ne: note._id
} // }
}); //});
const existRenote = null;
//#endregion
if (!existRenote) { if (!existRenote) {
// Update renoteee status // Update renoteee status
@ -365,48 +413,4 @@ export default async (user: IUser, data: {
}); });
} }
} }
// If has text content
if (data.text) {
// Extract an '@' mentions
const atMentions = tokens
.filter(t => t.type == 'mention')
.map(m => m.username)
// Drop dupulicates
.filter((v, i, s) => s.indexOf(v) == i);
// Resolve all mentions
await Promise.all(atMentions.map(async mention => {
// Fetch mentioned user
// SELECT _id
const mentionee = await User
.findOne({
usernameLower: mention.toLowerCase()
}, { _id: true });
// When mentioned user not found
if (mentionee == null) return;
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
if (data.reply && data.reply.userId.equals(mentionee._id)) return;
if (data.renote && data.renote.userId.equals(mentionee._id)) return;
// Add mention
addMention(mentionee._id, 'mention');
// Create notification
notify(mentionee._id, user._id, 'mention', {
noteId: note._id
});
}));
}
// Append mentions data
if (mentions.length > 0) {
Note.update({ _id: note._id }, {
$set: {
mentions
}
});
}
}); });