Compare commits

..

18 Commits

Author SHA1 Message Date
d8620187ec 10.66.2 2018-12-24 05:40:33 +09:00
520849d070 Update translation placeholder (#3735) 2018-12-24 05:36:02 +09:00
b6a028a8ed [API] Fix #3737 2018-12-24 05:31:20 +09:00
af5839bb59 Fix translation location (#3734) 2018-12-24 01:25:28 +09:00
a53e0d9f73 Fix error in featuredNotes (#3730) 2018-12-23 23:23:56 +09:00
49921f2dcf Fix: can not update remote Misskey user (#3731) 2018-12-23 23:23:17 +09:00
6b947c2139 Fix mention links (#3728)
canonical already starts with @, so remove the extra @.
2018-12-23 14:35:18 +09:00
98acf919f1 ダイレクト投稿でユーザーが指定されていなかったらrejectする (#3724) 2018-12-23 04:17:42 +09:00
c9c2853150 ダイレクトでメンションでもユーザーを指定できるように (#3722) 2018-12-23 03:44:18 +09:00
2bc708f8e6 Fix #3717 (#3723) 2018-12-23 03:41:28 +09:00
874b8fc3c2 Fix indent 2018-12-23 03:31:11 +09:00
7d6aac3431 Fix space 2018-12-23 03:27:26 +09:00
e2fc7decad 本文からメンション等を展開しないオプション (#3721) 2018-12-23 03:25:33 +09:00
21bed71f5e Rename PULL_REQUEST_TEMPLATE.md to .github/PULL_REQUEST_TEMPLATE.md 2018-12-22 20:43:44 +09:00
3b974428fc 10.66.1 2018-12-22 00:59:40 +09:00
580191fb17 Improve MFM bracket matching
Co-authored-by: syuilo <syuilotan@yahoo.co.jp>
2018-12-22 00:44:38 +09:00
be0cb88b6c Fix sharedInbox location (#3711)
* Fix sharedInbox location

* Perform update Following

* Fix comment
2018-12-22 00:12:34 +09:00
95c4e4497e Fix tag not found (#3710) 2018-12-21 21:46:50 +09:00
20 changed files with 201 additions and 51 deletions

View File

@ -1,6 +1,22 @@
ChangeLog
=========
10.66.2
-------
* i18nの修正
* ドライブのファイル一覧取得APIでファイルサイズによるソートが機能していなかった問題を修正
* リモートユーザーの更新時に、各ピン留め投稿の取得失敗は無視するように
* リモートMisskeyユーザーの情報が登録/更新出来なくなっていたのを修正
* メンションのリンク先URLに余計な@がプリフィクスされていたのを修正
* ダイレクトでリプライする際、リプライ先のユーザーは自動的に公開先として追加するように
* ダイレクトでメンションでもユーザーを指定できるように
10.66.1
-------
* ActivityPubのsharedInboxに関して修正
* MFMでのカッコの判定を改善
* バグ修正
10.66.0
-------
* ユーザーごとのRSSフィードを提供するように

View File

@ -805,6 +805,7 @@ desktop/views/components/settings.vue:
update-settings: "Advanced settings"
prevent-update: "Postpone updates (not recommended)"
prevent-update-desc: "Even if you turn this setting on, updates may apply. This setting is enabled only for this device."
mark-as-read-all-unread-notes: "Mark all posts as read"
no-updates: "No updates available"
no-updates-desc: "Your Misskey is up to date."
update-available: "A new version is available"
@ -1430,7 +1431,6 @@ mobile/views/pages/settings.vue:
signout: "Sign out"
sound: "Sounds"
enable-sounds: "Enable sounds"
mark-as-read-all-unread-notes: "Mark all posts as read"
password: "Password"
mobile/views/pages/user.vue:
follows-you: "Follows you"

View File

@ -838,6 +838,7 @@ desktop/views/components/settings.vue:
2fa: "二段階認証"
other: "その他"
license: "ライセンス"
mark-as-read-all-unread-notes: "すべての投稿を既読にする"
theme: "テーマ"
behaviour: "動作"
@ -1641,7 +1642,6 @@ mobile/views/pages/settings.vue:
signout: "サインアウト"
sound: "サウンド"
enable-sounds: "サウンドを有効にする"
mark-as-read-all-unread-notes: "すべての投稿を既読にする"
password: "パスワード"
mobile/views/pages/user.vue:

View File

@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.66.0",
"clientVersion": "2.0.12855",
"version": "10.66.2",
"clientVersion": "2.0.12873",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,

View File

@ -1,5 +1,5 @@
<template>
<router-link class="ldlomzub" :to="`/@${ canonical }`" v-user-preview="canonical">
<router-link class="ldlomzub" :to="`/${ canonical }`" v-user-preview="canonical">
<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
<span class="main">
<span class="username">@{{ username }}</span>

View File

@ -85,7 +85,7 @@ export default Vue.extend({
}
} else {
if (items[0].kind == 'file') {
alert('%i18n:only-one-file-attached%');
alert(this.$t('only-one-file-attached'));
}
}
},
@ -107,7 +107,7 @@ export default Vue.extend({
return;
} else if (e.dataTransfer.files.length > 1) {
e.preventDefault();
alert('%i18n:only-one-file-attached%');
alert(this.$t('only-one-file-attached'));
return;
}

View File

@ -207,9 +207,8 @@ export default Vue.extend({
this.visibility = this.reply.visibility;
}
// ダイレクトへのリプライはリプライ先ユーザーを初期設定
if (this.reply && this.reply.visibility === 'specified') {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
if (this.reply) {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
this.visibleUsers.push(user);
});
}
@ -236,7 +235,7 @@ export default Vue.extend({
},
methods: {
trimmedLength(text: string) {
trimmedLength(text: string) {
return length(text.trim());
},

View File

@ -3,7 +3,7 @@
<header :class="$style.header">
<h1>#{{ $route.params.tag }}</h1>
</header>
<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
</mk-ui>
</template>

View File

@ -197,9 +197,8 @@ export default Vue.extend({
this.visibility = this.reply.visibility;
}
// ダイレクトへのリプライはリプライ先ユーザーを初期設定
if (this.reply && this.reply.visibility === 'specified') {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
if (this.reply) {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
this.visibleUsers.push(user);
});
}

View File

@ -3,7 +3,7 @@
<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span>
<main>
<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
<mk-notes ref="timeline" :more="existMore ? more : null"/>
</main>
</mk-ui>

View File

@ -1,7 +1,7 @@
import * as P from 'parsimmon';
import parseAcct from '../misc/acct/parse';
import { toUnicode } from 'punycode';
import { takeWhile } from '../prelude/array';
import { takeWhile, cumulativeSum } from '../prelude/array';
import { Tree } from '../prelude/tree';
import * as T from '../prelude/tree';
@ -42,30 +42,18 @@ export function createTree(type: string, children: MfmForest, props: any): MfmTr
return T.createTree({ type, props }, children);
}
function getTrailingPosition(x: string): number {
const brackets = [
['(', ')'],
['', '」'],
];
const pendingBrackets = [] as any;
const end = x.split('').findIndex(char => {
const closeMatch = brackets.map(x => x[1]).indexOf(char);
const openMatch = brackets.map(x => x[0]).indexOf(char);
if (closeMatch != -1) {
if (pendingBrackets[closeMatch] > 0) {
pendingBrackets[closeMatch]--;
return false;
} else {
return true;
}
} else if (openMatch != -1) {
pendingBrackets[openMatch] = (pendingBrackets[openMatch] || 0) + 1;
return false;
} else {
return false;
}
});
return end > 0 ? end : x.length;
export function removeOrphanedBrackets(s: string): string {
const openBrackets = ['(', '「'];
const closeBrackets = [')', ''];
const xs = cumulativeSum(s.split('').map(c => {
if (openBrackets.includes(c)) return 1;
if (closeBrackets.includes(c)) return -1;
return 0;
}));
const firstOrphanedCloseBracket = xs.findIndex(x => x < 0);
if (firstOrphanedCloseBracket !== -1) return s.substr(0, firstOrphanedCloseBracket);
const lastMatched = xs.lastIndexOf(0);
return s.substr(0, lastMatched + 1);
}
const newline = P((input, i) => {
@ -220,7 +208,7 @@ const mfm = P.createLanguage({
const match = text.match(/^#([^\s\.,!\?#]+)/i);
if (!match) return P.makeFailure(i, 'not a hashtag');
let hashtag = match[1];
hashtag = hashtag.substr(0, getTrailingPosition(hashtag));
hashtag = removeOrphanedBrackets(hashtag);
if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag');
if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a hashtag');
if (hashtag.length > 50) return P.makeFailure(i, 'not a hashtag');
@ -390,7 +378,7 @@ const mfm = P.createLanguage({
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
if (!match) return P.makeFailure(i, 'not a url');
let url = match[0];
url = url.substr(0, getTrailingPosition(url));
url = removeOrphanedBrackets(url);
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
return P.makeSuccess(i + url.length, url);

View File

@ -109,3 +109,9 @@ export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
}
return ys;
}
export function cumulativeSum(xs: number[]): number[] {
const ys = Array.from(xs); // deep copy
for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
return ys;
}

View File

@ -18,6 +18,7 @@ import Instance from '../../../models/instance';
import getDriveFileUrl from '../../../misc/get-drive-file-url';
import { IEmoji } from '../../../models/emoji';
import { ITag } from './tag';
import Following from '../../../models/following';
const log = debug('misskey:activitypub');
@ -164,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
publicKeyPem: person.publicKey.publicKeyPem
},
inbox: person.inbox,
sharedInbox: person.sharedInbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
endpoints: person.endpoints,
uri: person.id,
@ -340,7 +341,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
$set: {
lastFetchedAt: new Date(),
inbox: person.inbox,
sharedInbox: person.sharedInbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null,
@ -368,6 +369,15 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
}
});
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await Following.update({
followerId: exist._id
}, {
$set: {
'_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
}
});
await updateFeatured(exist._id).catch(err => console.log(err));
}
@ -431,7 +441,7 @@ export async function updateFeatured(userId: mongo.ObjectID) {
await User.update({ _id: user._id }, {
$set: {
pinnedNoteIds: featuredNotes.map(note => note._id)
pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
}
});
}

View File

@ -63,6 +63,7 @@ export default async (user: ILocalUser) => {
following: `${id}/following`,
featured: `${id}/collections/featured`,
sharedInbox: `${config.url}/inbox`,
endpoints: { sharedInbox: `${config.url}/inbox` },
url: `${config.url}/@${user.username}`,
preferredUsername: user.username,
name: user.name,

View File

@ -56,7 +56,7 @@ export interface IPerson extends IObject {
following: any;
featured?: any;
outbox: any;
endpoints: string[];
endpoints: any;
}
export const isCollection = (object: IObject): object is ICollection =>

View File

@ -52,9 +52,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
_sort = {
length: -1
};
} else if (ps.sort == '+size') {
} else if (ps.sort == '-size') {
_sort = {
length: -1
length: 1
};
}
} else {

View File

@ -42,7 +42,7 @@ export const meta = {
},
visibleUserIds: {
validator: $.arr($.type(ID)).optional.unique().min(1),
validator: $.arr($.type(ID)).optional.unique().min(0),
transform: transformMany,
desc: {
'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
@ -82,6 +82,30 @@ export const meta = {
}
},
noExtractMentions: {
validator: $.bool.optional,
default: false,
desc: {
'ja-JP': '本文からメンションを展開しないか否か。'
}
},
noExtractHashtags: {
validator: $.bool.optional,
default: false,
desc: {
'ja-JP': '本文からハッシュタグを展開しないか否か。'
}
},
noExtractEmojis: {
validator: $.bool.optional,
default: false,
desc: {
'ja-JP': '本文からカスタム絵文字を展開しないか否か。'
}
},
geo: {
validator: $.obj({
coordinates: $.arr().length(2)
@ -237,6 +261,9 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
localOnly: ps.localOnly,
visibility: ps.visibility,
visibleUsers,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
geo: ps.geo
})
.then(note => pack(note, user))

View File

@ -182,6 +182,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
mentionedUsers.push(u);
}
}
for (const u of mentionedUsers) {
if (!data.visibleUsers.some(x => x._id.equals(u._id))) {
data.visibleUsers.push(u);
}
}
// ダイレクト投稿でユーザーが指定されていなかったらreject
if (data.visibleUsers.length === 0) {
return rej('Target user is not specified');
}
}
const note = await insertNote(user, data, tags, emojis, mentionedUsers);

View File

@ -6,7 +6,7 @@ import * as assert from 'assert';
import analyze from '../src/mfm/parse';
import toHtml from '../src/mfm/html';
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/parser';
import { createTree as tree, createLeaf as leaf, MfmTree, removeOrphanedBrackets } from '../src/mfm/parser';
function text(text: string): MfmTree {
return leaf('text', { text });
@ -49,6 +49,99 @@ describe('createTree', () => {
});
});
describe('removeOrphanedBrackets', () => {
it('single (contained)', () => {
const input = '(foo)';
const expected = '(foo)';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('single (head)', () => {
const input = '(foo)bar';
const expected = '(foo)bar';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('single (tail)', () => {
const input = 'foo(bar)';
const expected = 'foo(bar)';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('a', () => {
const input = '(foo';
const expected = '';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('b', () => {
const input = ')foo';
const expected = '';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('nested', () => {
const input = 'foo(「(bar)」)';
const expected = 'foo(「(bar)」)';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('no brackets', () => {
const input = 'foo';
const expected = 'foo';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('with foreign bracket (single)', () => {
const input = 'foo(bar))';
const expected = 'foo(bar)';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('with foreign bracket (open)', () => {
const input = 'foo(bar';
const expected = 'foo';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('with foreign bracket (close)', () => {
const input = 'foo)bar';
const expected = 'foo';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('with foreign bracket (close and open)', () => {
const input = 'foo)(bar';
const expected = 'foo';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('various bracket type', () => {
const input = 'foo「(bar)」(';
const expected = 'foo「(bar)」';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
it('intersected', () => {
const input = 'foo(「)」';
const expected = 'foo(「)」';
const actual = removeOrphanedBrackets(input);
assert.deepStrictEqual(actual, expected);
});
});
describe('MFM', () => {
it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');