Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina
2021-11-12 19:22:59 +09:00
64 changed files with 158 additions and 437 deletions

View File

@@ -0,0 +1,13 @@
const { MigrationInterface, QueryRunner } = require("typeorm");
module.exports = class removeViaMobile1636697408073 {
name = 'removeViaMobile1636697408073'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "viaMobile"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "viaMobile" boolean NOT NULL DEFAULT false`);
}
}

View File

@@ -59,7 +59,6 @@
"@types/redis": "2.8.32",
"@types/rename": "1.0.4",
"@types/request-stats": "3.0.0",
"@types/rimraf": "3.0.2",
"@types/seedrandom": "2.4.28",
"@types/sharp": "0.29.3",
"@types/sinonjs__fake-timers": "6.0.4",
@@ -162,7 +161,6 @@
"rename": "1.0.4",
"request-stats": "3.0.0",
"require-all": "3.0.0",
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"seedrandom": "3.0.5",

View File

@@ -215,11 +215,27 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false)
}
export async function resetDb() {
const conn = await getConnection();
const tables = await conn.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
AND C.relkind = 'r'
AND nspname !~ '^pg_toast';`);
await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
const reset = async () => {
const conn = await getConnection();
const tables = await conn.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
AND C.relkind = 'r'
AND nspname !~ '^pg_toast';`);
await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
};
for (let i = 1; i <= 3; i++) {
try {
await reset();
} catch (e) {
if (i === 3) {
throw e;
} else {
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
}
break;
}
}

View File

@@ -81,11 +81,6 @@ export class Note {
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false
})
public viaMobile: boolean;
@Column('boolean', {
default: false
})

View File

@@ -230,7 +230,6 @@ export class NoteRepository extends Repository<Note> {
visibility: note.visibility,
localOnly: note.localOnly || undefined,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
viaMobile: note.viaMobile || undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: convertLegacyReactions(note.reactions),
@@ -377,10 +376,6 @@ export const packedNoteSchema = {
optional: true as const, nullable: true as const,
ref: 'Note' as const,
},
viaMobile: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,
},
isHidden: {
type: 'boolean' as const,
optional: true as const, nullable: false as const,

View File

@@ -46,18 +46,18 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
});
let exportedNotesCount = 0;
let cursor: any = null;
let cursor: Note['id'] | null = null;
while (true) {
const notes = await Notes.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {})
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1
}
id: 1,
},
});
if (notes.length === 0) {
@@ -115,7 +115,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
done();
}
function serialize(note: Note, poll: Poll | null = null): any {
function serialize(note: Note, poll: Poll | null = null): Record<string, unknown> {
return {
id: note.id,
text: note.text,
@@ -125,9 +125,8 @@ function serialize(note: Note, poll: Poll | null = null): any {
renoteId: note.renoteId,
poll: poll,
cw: note.cw,
viaMobile: note.viaMobile,
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
localOnly: note.localOnly
localOnly: note.localOnly,
};
}

View File

@@ -250,7 +250,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
name: note.name,
cw,
text,
viaMobile: false,
localOnly: false,
visibility,
visibleUsers,

View File

@@ -57,11 +57,6 @@ export const meta = {
validator: $.optional.nullable.str.pipe(Notes.validateCw),
},
viaMobile: {
validator: $.optional.bool,
default: false,
},
localOnly: {
validator: $.optional.bool,
default: false,
@@ -283,7 +278,6 @@ export default define(meta, async (ps, user) => {
reply,
renote,
cw: ps.cw,
viaMobile: ps.viaMobile,
localOnly: ps.localOnly,
visibility: ps.visibility,
visibleUsers,

View File

@@ -23,6 +23,7 @@ const _filename = __filename;
const _dirname = dirname(_filename);
const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../client/assets/`;
const assets = `${_dirname}/../../../../../built/_client_dist_/`;
// Init app
@@ -59,6 +60,13 @@ router.get('/static-assets/(.*)', async ctx => {
});
});
router.get('/client-assets/(.*)', async ctx => {
await send(ctx as any, ctx.path.replace('/client-assets/', ''), {
root: clientAssets,
maxage: ms('7 days'),
});
});
router.get('/assets/(.*)', async ctx => {
await send(ctx as any, ctx.path.replace('/assets/', ''), {
root: assets,

View File

@@ -98,7 +98,6 @@ type Option = {
renote?: Note | null;
files?: DriveFile[] | null;
poll?: IPoll | null;
viaMobile?: boolean | null;
localOnly?: boolean | null;
cw?: string | null;
visibility?: string;
@@ -131,7 +130,6 @@ export default async (user: { id: User['id']; username: User['username']; host:
if (data.createdAt == null) data.createdAt = new Date();
if (data.visibility == null) data.visibility = 'public';
if (data.viaMobile == null) data.viaMobile = false;
if (data.localOnly == null) data.localOnly = false;
if (data.channel != null) data.visibility = 'public';
if (data.channel != null) data.visibleUsers = [];
@@ -478,7 +476,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
tags: tags.map(tag => normalizeForSearch(tag)),
emojis,
userId: user.id,
viaMobile: data.viaMobile!,
localOnly: data.localOnly!,
visibility: data.visibility as any,
visibleUserIds: data.visibility == 'specified'

View File

@@ -0,0 +1,7 @@
{
"env": {
"node": true,
"mocha": true,
"commonjs": true
}
}

View File

@@ -0,0 +1,94 @@
process.env.NODE_ENV = 'test';
import rndstr from 'rndstr';
import * as assert from 'assert';
import { initTestDb } from './utils';
describe('ActivityPub', () => {
before(async () => {
await initTestDb();
});
describe('Parse minimum object', () => {
const host = 'https://host1.test';
const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const actor = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: actorId,
type: 'Person',
preferredUsername,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
const post = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `${host}/users/${rndstr('0-9a-z', 8)}`,
type: 'Note',
attributedTo: actor.id,
to: 'https://www.w3.org/ns/activitystreams#Public',
content: 'あ',
};
it('Minimum Actor', async () => {
const { MockResolver } = await import('./misc/mock-resolver');
const { createPerson } = await import('../src/remote/activitypub/models/person');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.uri, actor.id);
assert.deepStrictEqual(user.username, actor.preferredUsername);
assert.deepStrictEqual(user.inbox, actor.inbox);
});
it('Minimum Note', async () => {
const { MockResolver } = await import('./misc/mock-resolver');
const { createNote } = await import('../src/remote/activitypub/models/note');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
resolver._register(post.id, post);
const note = await createNote(post.id, resolver, true);
assert.deepStrictEqual(note?.uri, post.id);
assert.deepStrictEqual(note?.visibility, 'public');
assert.deepStrictEqual(note?.text, post.content);
});
});
describe('Truncate long name', () => {
const host = 'https://host1.test';
const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`;
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
const name = rndstr('0-9a-z', 129);
const actor = {
'@context': 'https://www.w3.org/ns/activitystreams',
id: actorId,
type: 'Person',
preferredUsername,
name,
inbox: `${actorId}/inbox`,
outbox: `${actorId}/outbox`,
};
it('Actor', async () => {
const { MockResolver } = await import('./misc/mock-resolver');
const { createPerson } = await import('../src/remote/activitypub/models/person');
const resolver = new MockResolver();
resolver._register(actor.id, actor);
const user = await createPerson(actor.id, resolver);
assert.deepStrictEqual(user.name, actor.name.substr(0, 128));
});
});
});

View File

@@ -0,0 +1,55 @@
import * as assert from 'assert';
import { genRsaKeyPair } from '../src/misc/gen-key-pair';
import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request';
const httpSignature = require('http-signature');
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return {
scheme: 'Signature',
params: {
keyId: 'KeyID', // dummy, not used for verify
algorithm: algorithm,
headers: [ '(request-target)', 'date', 'host', 'digest' ], // dummy, not used for verify
signature: signature,
},
signingString: signingString,
algorithm: algorithm?.toUpperCase(),
keyId: 'KeyID', // dummy, not used for verify
};
};
describe('ap-request', () => {
it('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/inbox';
const activity = { a: 1 };
const body = JSON.stringify(activity);
const headers = {
'User-Agent': 'UA'
};
const req = createSignedPost({ key, url, body, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
it('createSignedGet with verify', async () => {
const keypair = await genRsaKeyPair();
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/outbox';
const headers = {
'User-Agent': 'UA'
};
const req = createSignedGet({ key, url, additionalHeaders: headers });
const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256');
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
});

View File

@@ -0,0 +1,476 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, startServer, shutdownServer } from './utils';
describe('API visibility', () => {
let p: childProcess.ChildProcess;
before(async () => {
p = await startServer();
});
after(async () => {
await shutdownServer(p);
});
describe('Note visibility', async () => {
//#region vars
/** ヒロイン */
let alice: any;
/** フォロワー */
let follower: any;
/** 非フォロワー */
let other: any;
/** 非フォロワーでもリプライやメンションをされた人 */
let target: any;
/** specified mentionでmentionを飛ばされる人 */
let target2: any;
/** public-post */
let pub: any;
/** home-post */
let home: any;
/** followers-post */
let fol: any;
/** specified-post */
let spe: any;
/** public-reply to target's post */
let pubR: any;
/** home-reply to target's post */
let homeR: any;
/** followers-reply to target's post */
let folR: any;
/** specified-reply to target's post */
let speR: any;
/** public-mention to target */
let pubM: any;
/** home-mention to target */
let homeM: any;
/** followers-mention to target */
let folM: any;
/** specified-mention to target */
let speM: any;
/** reply target post */
let tgt: any;
//#endregion
const show = async (noteId: any, by: any) => {
return await request('/notes/show', {
noteId
}, by);
};
before(async () => {
//#region prepare
// signup
alice = await signup({ username: 'alice' });
follower = await signup({ username: 'follower' });
other = await signup({ username: 'other' });
target = await signup({ username: 'target' });
target2 = await signup({ username: 'target2' });
// follow alice <= follower
await request('/following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
home = await post(alice, { text: 'x', visibility: 'home' });
fol = await post(alice, { text: 'x', visibility: 'followers' });
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
// replies
tgt = await post(target, { text: 'y', visibility: 'public' });
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
// mentions
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
//#endregion
});
//#region show post
// public
it('[show] public-postを自分が見れる', async(async () => {
const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postをフォロワーが見れる', async(async () => {
const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを非フォロワーが見れる', async(async () => {
const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを未認証が見れる', async(async () => {
const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-postを自分が見れる', async(async () => {
const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postをフォロワーが見れる', async(async () => {
const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを非フォロワーが見れる', async(async () => {
const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを未認証が見れる', async(async () => {
const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-postを自分が見れる', async(async () => {
const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postをフォロワーが見れる', async(async () => {
const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postを非フォロワーが見れない', async(async () => {
const res = await show(fol.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-postを未認証が見れない', async(async () => {
const res = await show(fol.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-postを自分が見れる', async(async () => {
const res = await show(spe.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postを指定ユーザーが見れる', async(async () => {
const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postをフォロワーが見れない', async(async () => {
const res = await show(spe.id, follower);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-postを非フォロワーが見れない', async(async () => {
const res = await show(spe.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-postを未認証が見れない', async(async () => {
const res = await show(spe.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
//#region show reply
// public
it('[show] public-replyを自分が見れる', async(async () => {
const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをされた人が見れる', async(async () => {
const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをフォロワーが見れる', async(async () => {
const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを非フォロワーが見れる', async(async () => {
const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを未認証が見れる', async(async () => {
const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-replyを自分が見れる', async(async () => {
const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをされた人が見れる', async(async () => {
const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをフォロワーが見れる', async(async () => {
const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを非フォロワーが見れる', async(async () => {
const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを未認証が見れる', async(async () => {
const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-replyを自分が見れる', async(async () => {
const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => {
const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyをフォロワーが見れる', async(async () => {
const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーが見れない', async(async () => {
const res = await show(folR.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-replyを未認証が見れない', async(async () => {
const res = await show(folR.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-replyを自分が見れる', async(async () => {
const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyを指定ユーザーが見れる', async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをフォロワーが見れない', async(async () => {
const res = await show(speR.id, follower);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-replyを非フォロワーが見れない', async(async () => {
const res = await show(speR.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-replyを未認証が見れない', async(async () => {
const res = await show(speR.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
//#region show mention
// public
it('[show] public-mentionを自分が見れる', async(async () => {
const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをされた人が見れる', async(async () => {
const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをフォロワーが見れる', async(async () => {
const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを非フォロワーが見れる', async(async () => {
const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを未認証が見れる', async(async () => {
const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// home
it('[show] home-mentionを自分が見れる', async(async () => {
const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをされた人が見れる', async(async () => {
const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをフォロワーが見れる', async(async () => {
const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを非フォロワーが見れる', async(async () => {
const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを未認証が見れる', async(async () => {
const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// followers
it('[show] followers-mentionを自分が見れる', async(async () => {
const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async(async () => {
const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionをフォロワーが見れる', async(async () => {
const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
const res = await show(folM.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] followers-mentionを未認証が見れない', async(async () => {
const res = await show(folM.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
// specified
it('[show] specified-mentionを自分が見れる', async(async () => {
const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified-mentionを指定ユーザーが見れる', async(async () => {
const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => {
const res = await show(speM.id, target2);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionをフォロワーが見れない', async(async () => {
const res = await show(speM.id, follower);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
const res = await show(speM.id, other);
assert.strictEqual(res.body.isHidden, true);
}));
it('[show] specified-mentionを未認証が見れない', async(async () => {
const res = await show(speM.id, null);
assert.strictEqual(res.body.isHidden, true);
}));
//#endregion
//#region HTL
it('[HTL] public-post が 自分が見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[HTL] public-post が 非フォロワーから見れない', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes.length, 0);
}));
it('[HTL] followers-post が フォロワーから見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == fol.id);
assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region RTL
it('[replies] followers-reply が フォロワーから見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes.length, 0);
}));
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
//#endregion
//#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folM.id);
assert.strictEqual(notes[0].text, '@target x');
}));
//#endregion
});
});

View File

@@ -0,0 +1,970 @@
/*
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, uploadFile } from './utils';
describe('API', () => {
let p: childProcess.ChildProcess;
beforeEach(done => {
p = childProcess.spawn('node', [__dirname + '/../index.js'], {
stdio: ['inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test' }
});
p.on('message', message => {
if (message === 'ok') {
done();
}
});
});
afterEach(() => {
p.kill();
});
describe('signup', () => {
it('不正なユーザー名でアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test.',
password: 'test'
});
assert.strictEqual(res.status, 400);
}));
it('空のパスワードでアカウントが作成できない', async(async () => {
const res = await request('/signup', {
username: 'test',
password: ''
});
assert.strictEqual(res.status, 400);
}));
it('正しくアカウントが作成できる', async(async () => {
const me = {
username: 'test',
password: 'test'
};
const res = await request('/signup', me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.username, me.username);
}));
it('同じユーザー名のアカウントは作成できない', async(async () => {
await signup({
username: 'test'
});
const res = await request('/signup', {
username: 'test',
password: 'test'
});
assert.strictEqual(res.status, 400);
}));
});
describe('signin', () => {
it('間違ったパスワードでサインインできない', async(async () => {
await signup({
username: 'test',
password: 'foo'
});
const res = await request('/signin', {
username: 'test',
password: 'bar'
});
assert.strictEqual(res.status, 403);
}));
it('クエリをインジェクションできない', async(async () => {
await signup({
username: 'test'
});
const res = await request('/signin', {
username: 'test',
password: {
$gt: ''
}
});
assert.strictEqual(res.status, 400);
}));
it('正しい情報でサインインできる', async(async () => {
await signup({
username: 'test',
password: 'foo'
});
const res = await request('/signin', {
username: 'test',
password: 'foo'
});
assert.strictEqual(res.status, 200);
}));
});
describe('i/update', () => {
it('アカウント設定を更新できる', async(async () => {
const me = await signup();
const myName = '大室櫻子';
const myLocation = '七森中';
const myBirthday = '2000-09-07';
const res = await request('/i/update', {
name: myName,
location: myLocation,
birthday: myBirthday
}, me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, myName);
assert.strictEqual(res.body.location, myLocation);
assert.strictEqual(res.body.birthday, myBirthday);
}));
it('名前を空白にできない', async(async () => {
const me = await signup();
const res = await request('/i/update', {
name: ' '
}, me);
assert.strictEqual(res.status, 400);
}));
it('誕生日の設定を削除できる', async(async () => {
const me = await signup();
await request('/i/update', {
birthday: '2000-09-07'
}, me);
const res = await request('/i/update', {
birthday: null
}, me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.birthday, null);
}));
it('不正な誕生日の形式で怒られる', async(async () => {
const me = await signup();
const res = await request('/i/update', {
birthday: '2000/09/07'
}, me);
assert.strictEqual(res.status, 400);
}));
});
describe('users/show', () => {
it('ユーザーが取得できる', async(async () => {
const me = await signup();
const res = await request('/users/show', {
userId: me.id
}, me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, me.id);
}));
it('ユーザーが存在しなかったら怒る', async(async () => {
const res = await request('/users/show', {
userId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/users/show', {
userId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/show', () => {
it('投稿が取得できる', async(async () => {
const me = await signup();
const myPost = await post(me, {
text: 'test'
});
const res = await request('/notes/show', {
noteId: myPost.id
}, me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.id, myPost.id);
assert.strictEqual(res.body.text, myPost.text);
}));
it('投稿が存在しなかったら怒る', async(async () => {
const res = await request('/notes/show', {
noteId: '000000000000000000000000'
});
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const res = await request('/notes/show', {
noteId: 'kyoppie'
});
assert.strictEqual(res.status, 400);
}));
});
describe('notes/reactions/create', () => {
it('リアクションできる', async(async () => {
const bob = await signup({ username: 'bob' });
const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: 'like'
}, alice);
assert.strictEqual(res.status, 204);
}));
it('自分の投稿にはリアクションできない', async(async () => {
const me = await signup();
const myPost = await post(me);
const res = await request('/notes/reactions/create', {
noteId: myPost.id,
reaction: 'like'
}, me);
assert.strictEqual(res.status, 400);
}));
it('二重にリアクションできない', async(async () => {
const bob = await signup({ username: 'bob' });
const bobPost = await post(bob);
const alice = await signup({ username: 'alice' });
await react(alice, bobPost, 'like');
const res = await request('/notes/reactions/create', {
noteId: bobPost.id,
reaction: 'like'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しない投稿にはリアクションできない', async(async () => {
const me = await signup();
const res = await request('/notes/reactions/create', {
noteId: '000000000000000000000000',
reaction: 'like'
}, me);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const me = await signup();
const res = await request('/notes/reactions/create', {}, me);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const me = await signup();
const res = await request('/notes/reactions/create', {
noteId: 'kyoppie',
reaction: 'like'
}, me);
assert.strictEqual(res.status, 400);
}));
});
describe('following/create', () => {
it('フォローできる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('既にフォローしている場合は怒る', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
await request('/following/create', {
userId: alice.id
}, bob);
const res = await request('/following/create', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォローできない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/create', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォローできない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/create', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/create', {
userId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('following/delete', () => {
it('フォロー解除できる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
await request('/following/create', {
userId: alice.id
}, bob);
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 200);
}));
it('フォローしていない場合は怒る', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const res = await request('/following/delete', {
userId: alice.id
}, bob);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーはフォロー解除できない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/delete', {
userId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('自分自身はフォロー解除できない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/delete', {
userId: alice.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('空のパラメータで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/delete', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/following/delete', {
userId: 'kyoppie'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive', () => {
it('ドライブ情報を取得できる', async(async () => {
const bob = await signup({ username: 'bob' });
await uploadFile({
userId: me.id,
size: 256
});
await uploadFile({
userId: me.id,
size: 512
});
await uploadFile({
userId: me.id,
size: 1024
});
const res = await request('/drive', {}, me);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).have.property('usage').eql(1792);
}));
});
describe('drive/files/create', () => {
it('ファイルを作成できる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await uploadFile(alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'Lenna.png');
}));
it('ファイルに名前を付けられる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await assert.request(server)
.post('/drive/files/create')
.field('i', alice.token)
.field('name', 'Belmond.png')
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
expect(res).have.status(200);
expect(res.body).be.a('object');
expect(res.body).have.property('name').eql('Belmond.png');
}));
it('ファイル無しで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/files/create', {}, alice);
assert.strictEqual(res.status, 400);
}));
it('SVGファイルを作成できる', async(async () => {
const izumi = await signup({ username: 'izumi' });
const res = await uploadFile(izumi, __dirname + '/resources/image.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'image.svg');
assert.strictEqual(res.body.type, 'image/svg+xml');
}));
});
describe('drive/files/update', () => {
it('名前を更新できる', async(async () => {
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const newName = 'いちごパスタ.png';
const res = await request('/drive/files/update', {
fileId: file.id,
name: newName
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, newName);
}));
it('他人のファイルは更新できない', async(async () => {
const bob = await signup({ username: 'bob' });
const alice = await signup({ username: 'alice' });
const file = await uploadFile(bob);
const res = await request('/drive/files/update', {
fileId: file.id,
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, folder.id);
}));
it('親フォルダを無しにできる', async(async () => {
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.folderId, null);
}));
it('他人のフォルダには入れられない', async(async () => {
const bob = await signup({ username: 'bob' });
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: folder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const file = await uploadFile(alice);
const res = await request('/drive/files/update', {
fileId: file.id,
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('ファイルが存在しなかったら怒る', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/files/update', {
fileId: '000000000000000000000000',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('間違ったIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/files/update', {
fileId: 'kyoppie',
name: 'いちごパスタ.png'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('drive/folders/create', () => {
it('フォルダを作成できる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/folders/create', {
name: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'test');
}));
});
describe('drive/folders/update', () => {
it('名前を更新できる', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.name, 'new name');
}));
it('他人のフォルダを更新できない', async(async () => {
const bob = await signup({ username: 'bob' });
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
name: 'new name'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('親フォルダを更新できる', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, parentFolder.id);
}));
it('親フォルダを無しに更新できる', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: null
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.parentId, null);
}));
it('他人のフォルダを親フォルダに設定できない', async(async () => {
const bob = await signup({ username: 'bob' });
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, bob)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const parentFolder = (await request('/drive/folders/create', {
name: 'parent'
}, alice)).body;
await request('/drive/folders/update', {
folderId: parentFolder.id,
parentId: folder.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: parentFolder.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
const alice = await signup({ username: 'alice' });
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderB = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const folderC = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
await request('/drive/folders/update', {
folderId: folderB.id,
parentId: folderA.id
}, alice);
await request('/drive/folders/update', {
folderId: folderC.id,
parentId: folderB.id
}, alice);
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderC.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('フォルダが循環するような構造にできない(自身)', async(async () => {
const arisugawa = await signup({ username: 'arisugawa' });
const folderA = (await request('/drive/folders/create', {
name: 'test'
}, arisugawa)).body;
const res = await request('/drive/folders/update', {
folderId: folderA.id,
parentId: folderA.id
}, arisugawa);
assert.strictEqual(res.status, 400);
}));
it('存在しない親フォルダを設定できない', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正な親フォルダIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const folder = (await request('/drive/folders/create', {
name: 'test'
}, alice)).body;
const res = await request('/drive/folders/update', {
folderId: folder.id,
parentId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないフォルダを更新できない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/folders/update', {
folderId: '000000000000000000000000'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なフォルダIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/drive/folders/update', {
folderId: 'foo'
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('messaging/messages/create', () => {
it('メッセージを送信できる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.text, 'test');
}));
it('自分自身にはメッセージを送信できない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/messaging/messages/create', {
userId: alice.id,
text: 'Yo'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにはメッセージを送信できない', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/messaging/messages/create', {
userId: '000000000000000000000000',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なユーザーIDで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await request('/messaging/messages/create', {
userId: 'foo',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
it('テキストが無くて怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const res = await request('/messaging/messages/create', {
userId: bob.id
}, alice);
assert.strictEqual(res.status, 400);
}));
it('文字数オーバーで怒られる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const res = await request('/messaging/messages/create', {
userId: bob.id,
text: '!'.repeat(1001)
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('notes/replies', () => {
it('自分に閲覧権限のない投稿は含まれない', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const carol = await signup({ username: 'carol' });
const alicePost = await post(alice, {
text: 'foo'
});
await post(bob, {
replyId: alicePost.id,
text: 'bar',
visibility: 'specified',
visibleUserIds: [alice.id]
});
const res = await request('/notes/replies', {
noteId: alicePost.id
}, carol);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 0);
}));
});
describe('notes/timeline', () => {
it('フォロワー限定投稿が含まれる', async(async () => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
await request('/following/create', {
userId: alice.id
}, bob);
const alicePost = await post(alice, {
text: 'foo',
visibility: 'followers'
});
const res = await request('/notes/timeline', {}, bob);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 1);
assert.strictEqual(res.body[0].id, alicePost.id);
}));
});
});
*/

View File

@@ -0,0 +1,85 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, startServer, shutdownServer } from './utils';
describe('Block', () => {
let p: childProcess.ChildProcess;
// alice blocks bob
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('Block作成', async(async () => {
const res = await request('/blocking/create', {
userId: bob.id
}, alice);
assert.strictEqual(res.status, 200);
}));
it('ブロックされているユーザーをフォローできない', async(async () => {
const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
}));
it('ブロックされているユーザーにリアクションできない', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
}));
it('ブロックされているユーザーに返信できない', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
}));
it('ブロックされているユーザーのートをRenoteできない', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
}));
// TODO: ユーザーリストに入れられないテスト
// TODO: ユーザーリストから除外されるテスト
it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request('/notes/local-timeline', {}, bob);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
}));
});

View File

@@ -0,0 +1,462 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as lolex from '@sinonjs/fake-timers';
import { async, initTestDb } from './utils';
import TestChart from '../src/services/chart/charts/classes/test';
import TestGroupedChart from '../src/services/chart/charts/classes/test-grouped';
import TestUniqueChart from '../src/services/chart/charts/classes/test-unique';
import * as _TestChart from '../src/services/chart/charts/schemas/test';
import * as _TestGroupedChart from '../src/services/chart/charts/schemas/test-grouped';
import * as _TestUniqueChart from '../src/services/chart/charts/schemas/test-unique';
import Chart from '../src/services/chart/core';
describe('Chart', () => {
let testChart: TestChart;
let testGroupedChart: TestGroupedChart;
let testUniqueChart: TestUniqueChart;
let clock: lolex.Clock;
beforeEach(async(async () => {
await initTestDb(false, [
Chart.schemaToEntity(_TestChart.name, _TestChart.schema),
Chart.schemaToEntity(_TestGroupedChart.name, _TestGroupedChart.schema),
Chart.schemaToEntity(_TestUniqueChart.name, _TestUniqueChart.schema)
]);
testChart = new TestChart();
testGroupedChart = new TestGroupedChart();
testUniqueChart = new TestUniqueChart();
clock = lolex.install({
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0))
});
}));
afterEach(async(async () => {
clock.uninstall();
}));
it('Can updates', async(async () => {
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
}));
it('Can updates (dec)', async(async () => {
await testChart.decrement();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [1, 0, 0],
inc: [0, 0, 0],
total: [-1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [1, 0, 0],
inc: [0, 0, 0],
total: [-1, 0, 0]
},
});
}));
it('Empty chart', async(async () => {
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0]
},
});
}));
it('Can updates at multiple times at same time', async(async () => {
await testChart.increment();
await testChart.increment();
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [3, 0, 0],
total: [3, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [3, 0, 0],
total: [3, 0, 0]
},
});
}));
it('Can updates at different times', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 1, 0],
total: [2, 1, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/*
it('Can updates at different times without save', async(async () => {
await testChart.increment();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 1, 0],
total: [2, 1, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
*/
it('Can padding', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('02:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 1],
total: [2, 1, 1]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
// 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('05:00:00');
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 1, 1]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
}));
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190
it('Can padding from past range 2', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('05:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [2, 1, 1]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
it('Can specify offset', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
it('Can specify offset (floor time)', async(async () => {
clock.tick('00:30:00');
await testChart.increment();
await testChart.save();
clock.tick('01:30:00');
await testChart.increment();
await testChart.save();
const chartHours = await testChart.getChart('hour', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
const chartDays = await testChart.getChart('day', 3, new Date(Date.UTC(2000, 0, 1, 0, 0, 0)));
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [2, 0, 0],
total: [2, 0, 0]
},
});
}));
describe('Grouped', () => {
it('Can updates', async(async () => {
await testGroupedChart.increment('alice');
await testGroupedChart.save();
const aliceChartHours = await testGroupedChart.getChart('hour', 3, null, 'alice');
const aliceChartDays = await testGroupedChart.getChart('day', 3, null, 'alice');
const bobChartHours = await testGroupedChart.getChart('hour', 3, null, 'bob');
const bobChartDays = await testGroupedChart.getChart('day', 3, null, 'bob');
assert.deepStrictEqual(aliceChartHours, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(aliceChartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(bobChartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0]
},
});
assert.deepStrictEqual(bobChartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [0, 0, 0]
},
});
}));
});
describe('Unique increment', () => {
it('Can updates', async(async () => {
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob');
await testUniqueChart.save();
const chartHours = await testUniqueChart.getChart('hour', 3, null);
const chartDays = await testUniqueChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: [2, 0, 0],
});
assert.deepStrictEqual(chartDays, {
foo: [2, 0, 0],
});
}));
});
describe('Resync', () => {
it('Can resync', async(async () => {
testChart.total = 1;
await testChart.resync();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 0, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [0, 0, 0],
total: [1, 0, 0]
},
});
}));
it('Can resync (2)', async(async () => {
await testChart.increment();
await testChart.save();
clock.tick('01:00:00');
testChart.total = 100;
await testChart.resync();
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
assert.deepStrictEqual(chartHours, {
foo: {
dec: [0, 0, 0],
inc: [0, 1, 0],
total: [100, 1, 0]
},
});
assert.deepStrictEqual(chartDays, {
foo: {
dec: [0, 0, 0],
inc: [1, 0, 0],
total: [100, 0, 0]
},
});
}));
});
});

View File

@@ -0,0 +1,15 @@
version: "3"
services:
redistest:
image: redis:4.0-alpine
ports:
- "127.0.0.1:56312:6379"
dbtest:
image: postgres:12.2-alpine
ports:
- "127.0.0.1:54312:5432"
environment:
POSTGRES_DB: "test-misskey"
POSTGRES_HOST_AUTH_METHOD: trust

View File

@@ -0,0 +1,42 @@
import * as assert from 'assert';
import { extractMentions } from '../src/misc/extract-mentions';
import { parse } from 'mfm-js';
describe('Extract mentions', () => {
it('simple', () => {
const ast = parse('@foo @bar @baz')!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
host: null
}, {
username: 'bar',
acct: '@bar',
host: null
}, {
username: 'baz',
acct: '@baz',
host: null
}]);
});
it('nested', () => {
const ast = parse('@foo **@bar** @baz')!;
const mentions = extractMentions(ast);
assert.deepStrictEqual(mentions, [{
username: 'foo',
acct: '@foo',
host: null
}, {
username: 'bar',
acct: '@bar',
host: null
}, {
username: 'baz',
acct: '@baz',
host: null
}]);
});
});

View File

@@ -0,0 +1,205 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils';
import * as openapi from '@redocly/openapi-core';
// Request Accept
const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*';
// Response Contet-Type
const AP = 'application/activity+json; charset=utf-8';
const JSON = 'application/json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';
describe('Fetch resource', () => {
let p: childProcess.ChildProcess;
let alice: any;
let alicesPost: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
alicesPost = await post(alice, {
text: 'test'
});
});
after(async () => {
await shutdownServer(p);
});
describe('Common', () => {
it('meta', async(async () => {
const res = await request('/meta', {
});
assert.strictEqual(res.status, 200);
}));
it('GET root', async(async () => {
const res = await simpleGet('/');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET docs', async(async () => {
const res = await simpleGet('/docs/ja-JP/about');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET api-doc', async(async () => {
const res = await simpleGet('/api-doc');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('GET api.json', async(async () => {
const res = await simpleGet('/api.json');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON);
}));
it('Validate api.json', async(async () => {
const config = await openapi.loadConfig();
const result = await openapi.bundle({
config,
ref: `http://localhost:${port}/api.json`
});
for (const problem of result.problems) {
console.log(`${problem.message} - ${problem.location[0]?.pointer}`);
}
assert.strictEqual(result.problems.length, 0);
}));
it('GET favicon.ico', async(async () => {
const res = await simpleGet('/favicon.ico');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/x-icon');
}));
it('GET apple-touch-icon.png', async(async () => {
const res = await simpleGet('/apple-touch-icon.png');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/png');
}));
it('GET twemoji svg', async(async () => {
const res = await simpleGet('/twemoji/2764.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
}));
it('GET twemoji svg with hyphen', async(async () => {
const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'image/svg+xml');
}));
});
describe('/@:username', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => HTML', async(async () => {
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('Unspecified => HTML', async(async () => {
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe('/users/:id', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => Redirect to /@:username', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
it('Undecided => HTML', async(async () => {
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 302);
assert.strictEqual(res.location, `/@${alice.username}`);
}));
});
describe('/notes/:id', () => {
it('Only AP => AP', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer AP => AP', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, AP);
}));
it('Prefer HTML => HTML', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
it('Unspecified => HTML', async(async () => {
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, HTML);
}));
});
describe('Feeds', () => {
it('RSS', async(async () => {
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8');
}));
it('ATOM', async(async () => {
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8');
}));
it('JSON', async(async () => {
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, 'application/json; charset=utf-8');
}));
});
});

View File

@@ -0,0 +1,167 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils';
describe('FF visibility', () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async(async () => {
await request('/i/update', {
ffVisibility: 'public',
}, alice);
const followingRes = await request('/users/following', {
userId: alice.id,
}, bob);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async(async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
const followingRes = await request('/users/following', {
userId: alice.id,
}, alice);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async(async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
const followingRes = await request('/users/following', {
userId: alice.id,
}, bob);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
assert.strictEqual(followersRes.status, 400);
}));
it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async(async () => {
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
await request('/following/create', {
userId: alice.id,
}, bob);
const followingRes = await request('/users/following', {
userId: alice.id,
}, bob);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async(async () => {
await request('/i/update', {
ffVisibility: 'private',
}, alice);
const followingRes = await request('/users/following', {
userId: alice.id,
}, alice);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}));
it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async(async () => {
await request('/i/update', {
ffVisibility: 'private',
}, alice);
const followingRes = await request('/users/following', {
userId: alice.id,
}, bob);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
assert.strictEqual(followersRes.status, 400);
}));
describe('AP', () => {
it('ffVisibility が public 以外ならばAPからは取得できない', async(async () => {
{
await request('/i/update', {
ffVisibility: 'public',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(followersRes.status, 200);
}
{
await request('/i/update', {
ffVisibility: 'followers',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
{
await request('/i/update', {
ffVisibility: 'private',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
}));
});
});

View File

@@ -0,0 +1,142 @@
import * as assert from 'assert';
import { async } from './utils';
import { getFileInfo } from '../src/misc/get-file-info';
describe('Get file info', () => {
it('Empty file', async (async () => {
const path = `${__dirname}/resources/emptyfile`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 0,
md5: 'd41d8cd98f00b204e9800998ecf8427e',
type: {
mime: 'application/octet-stream',
ext: null
},
width: undefined,
height: undefined,
});
}));
it('Generic JPEG', async (async () => {
const path = `${__dirname}/resources/Lenna.jpg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 25360,
md5: '091b3f259662aa31e2ffef4519951168',
type: {
mime: 'image/jpeg',
ext: 'jpg'
},
width: 512,
height: 512,
});
}));
it('Generic APNG', async (async () => {
const path = `${__dirname}/resources/anime.png`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 1868,
md5: '08189c607bea3b952704676bb3c979e0',
type: {
mime: 'image/apng',
ext: 'apng'
},
width: 256,
height: 256,
});
}));
it('Generic AGIF', async (async () => {
const path = `${__dirname}/resources/anime.gif`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 2248,
md5: '32c47a11555675d9267aee1a86571e7e',
type: {
mime: 'image/gif',
ext: 'gif'
},
width: 256,
height: 256,
});
}));
it('PNG with alpha', async (async () => {
const path = `${__dirname}/resources/with-alpha.png`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 3772,
md5: 'f73535c3e1e27508885b69b10cf6e991',
type: {
mime: 'image/png',
ext: 'png'
},
width: 256,
height: 256,
});
}));
it('Generic SVG', async (async () => {
const path = `${__dirname}/resources/image.svg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 505,
md5: 'b6f52b4b021e7b92cdd04509c7267965',
type: {
mime: 'image/svg+xml',
ext: 'svg'
},
width: 256,
height: 256,
});
}));
it('SVG with XML definition', async (async () => {
// https://github.com/misskey-dev/misskey/issues/4413
const path = `${__dirname}/resources/with-xml-def.svg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 544,
md5: '4b7a346cde9ccbeb267e812567e33397',
type: {
mime: 'image/svg+xml',
ext: 'svg'
},
width: 256,
height: 256,
});
}));
it('Dimension limit', async (async () => {
const path = `${__dirname}/resources/25000x25000.png`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 75933,
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
type: {
mime: 'application/octet-stream', // do not treat as image
ext: null
},
width: 25000,
height: 25000,
});
}));
});

View File

@@ -0,0 +1,89 @@
import * as assert from 'assert';
import * as mfm from 'mfm-js';
import { toHtml } from '../src/mfm/to-html';
import { fromHtml } from '../src/mfm/from-html';
describe('toHtml', () => {
it('br', () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output);
});
it('br alt', () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(mfm.parse(input)), output);
});
});
describe('fromHtml', () => {
it('p', () => {
assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb');
});
it('block element', () => {
assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb');
});
it('inline element', () => {
assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb');
});
it('block code', () => {
assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```');
});
it('inline code', () => {
assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`');
});
it('quote', () => {
assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b');
});
it('br', () => {
assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd');
});
it('link with different text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">c</a> d</p>'), 'a [c](https://example.com/b) d');
});
it('link with different text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">c</a> d</p>'), 'a [c](<https://example.com/ä>) d');
});
it('link with same text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b">https://example.com/b</a> d</p>'), 'a https://example.com/b d');
});
it('link with same text, but not encoded', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/ä">https://example.com/ä</a> d</p>'), 'a <https://example.com/ä> d');
});
it('link with no url', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="b">c</a> d</p>'), 'a [c](b) d');
});
it('link without href', () => {
assert.deepStrictEqual(fromHtml('<p>a <a>c</a> d</p>'), 'a c d');
});
it('link without text', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/b"></a> d</p>'), 'a https://example.com/b d');
});
it('link without both', () => {
assert.deepStrictEqual(fromHtml('<p>a <a></a> d</p>'), 'a d');
});
it('mention', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
});
it('hashtag', () => {
assert.deepStrictEqual(fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
});
});

View File

@@ -0,0 +1,35 @@
import Resolver from '../../src/remote/activitypub/resolver';
import { IObject } from '../../src/remote/activitypub/type';
type MockResponse = {
type: string;
content: string;
};
export class MockResolver extends Resolver {
private _rs = new Map<string, MockResponse>();
public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
this._rs.set(uri, {
type,
content: typeof content === 'string' ? content : JSON.stringify(content)
});
}
public async resolve(value: string | IObject): Promise<IObject> {
if (typeof value !== 'string') return value;
const r = this._rs.get(value);
if (!r) {
throw {
name: `StatusError`,
statusCode: 404,
message: `Not registed for mock`
};
}
const object = JSON.parse(r.content);
return object;
}
}

View File

@@ -0,0 +1,147 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils';
describe('Mute', () => {
let p: childProcess.ChildProcess;
// alice mutes carol
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('ミュート作成', async(async () => {
const res = await request('/mute/create', {
userId: carol.id
}, alice);
assert.strictEqual(res.status, 204);
}));
it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice hi' });
const carolNote = await post(carol, { text: '@alice hi' });
const res = await request('/notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
}));
it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await post(carol, { text: '@alice hi' });
const res = await request('/i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
let fired = false;
const ws = await connectStream(alice, 'main', ({ type }) => {
if (type == 'unreadMention') {
fired = true;
}
});
post(carol, { text: '@alice hi' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await request('/notifications/mark-all-as-read', {}, alice);
let fired = false;
const ws = await connectStream(alice, 'main', ({ type }) => {
if (type == 'unreadNotification') {
fired = true;
}
});
post(carol, { text: '@alice hi' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
describe('Timeline', () => {
it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);
const res = await request('/notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
}));
it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => {
const aliceNote = await post(alice);
const carolNote = await post(carol);
const bobNote = await post(bob, {
renoteId: carolNote.id
});
const res = await request('/notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
}));
});
describe('Notification', () => {
it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => {
const aliceNote = await post(alice);
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await request('/i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
}));
});
});

View File

@@ -0,0 +1,336 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils';
import { Note } from '../src/models/entities/note';
describe('Note', () => {
let p: childProcess.ChildProcess;
let Notes: any;
let alice: any;
let bob: any;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Notes = connection.getRepository(Note);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
});
after(async () => {
await shutdownServer(p);
});
it('投稿できる', async(async () => {
const post = {
text: 'test'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it('ファイルを添付できる', async(async () => {
const file = await uploadFile(alice);
const res = await request('/notes/create', {
fileIds: [file.id]
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
}));
it('他人のファイルは無視', async(async () => {
const file = await uploadFile(bob);
const res = await request('/notes/create', {
text: 'test',
fileIds: [file.id]
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('存在しないファイルは無視', async(async () => {
const res = await request('/notes/create', {
text: 'test',
fileIds: ['000000000000000000000000']
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('不正なファイルIDで怒られる', async(async () => {
const res = await request('/notes/create', {
fileIds: ['kyoppie']
}, alice);
assert.strictEqual(res.status, 400);
}));
it('返信できる', async(async () => {
const bobPost = await post(bob, {
text: 'foo'
});
const alicePost = {
text: 'bar',
replyId: bobPost.id
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
}));
it('renoteできる', async(async () => {
const bobPost = await post(bob, {
text: 'test'
});
const alicePost = {
renoteId: bobPost.id
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it('引用renoteできる', async(async () => {
const bobPost = await post(bob, {
text: 'test'
});
const alicePost = {
text: 'test',
renoteId: bobPost.id
};
const res = await request('/notes/create', alicePost, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, alicePost.text);
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
}));
it('文字数ぎりぎりで怒られない', async(async () => {
const post = {
text: '!'.repeat(500)
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
}));
it('文字数オーバーで怒られる', async(async () => {
const post = {
text: '!'.repeat(501)
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないリプライ先で怒られる', async(async () => {
const post = {
text: 'test',
replyId: '000000000000000000000000'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないrenote対象で怒られる', async(async () => {
const post = {
renoteId: '000000000000000000000000'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なリプライ先IDで怒られる', async(async () => {
const post = {
text: 'test',
replyId: 'foo'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('不正なrenote対象IDで怒られる', async(async () => {
const post = {
renoteId: 'foo'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
}));
it('存在しないユーザーにメンションできる', async(async () => {
const post = {
text: '@ghost yo'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
}));
it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
const post = {
text: '@bob @bob @bob yo'
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
const noteDoc = await Notes.findOne(res.body.createdNote.id);
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
}));
describe('notes/create', () => {
it('投票を添付できる', async(async () => {
const res = await request('/notes/create', {
text: 'test',
poll: {
choices: ['foo', 'bar']
}
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.poll != null, true);
}));
it('投票の選択肢が無くて怒られる', async(async () => {
const res = await request('/notes/create', {
poll: {}
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
const res = await request('/notes/create', {
poll: {
choices: []
}
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票の選択肢が1つで怒られる', async(async () => {
const res = await request('/notes/create', {
poll: {
choices: ['Strawberry Pasta']
}
}, alice);
assert.strictEqual(res.status, 400);
}));
it('投票できる', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako']
}
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1
}, alice);
assert.strictEqual(res.status, 204);
}));
it('複数投票できない', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako']
}
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2
}, alice);
assert.strictEqual(res.status, 400);
}));
it('許可されている場合は複数投票できる', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
multiple: true
}
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 0
}, alice);
await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1
}, alice);
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 2
}, alice);
assert.strictEqual(res.status, 204);
}));
it('締め切られている場合は投票できない', async(async () => {
const { body } = await request('/notes/create', {
text: 'test',
poll: {
choices: ['sakura', 'izumi', 'ako'],
expiredAfter: 1
}
}, alice);
await new Promise(x => setTimeout(x, 2));
const res = await request('/notes/polls/vote', {
noteId: body.createdNote.id,
choice: 1
}, alice);
assert.strictEqual(res.status, 400);
}));
});
});

View File

@@ -0,0 +1,18 @@
import * as assert from 'assert';
import { just, nothing } from '../../src/prelude/maybe';
describe('just', () => {
it('has a value', () => {
assert.deepStrictEqual(just(3).isJust(), true);
});
it('has the inverse called get', () => {
assert.deepStrictEqual(just(3).get(), 3);
});
});
describe('nothing', () => {
it('has no value', () => {
assert.deepStrictEqual(nothing().isJust(), false);
});
});

View File

@@ -0,0 +1,13 @@
import * as assert from 'assert';
import { query } from '../../src/prelude/url';
describe('url', () => {
it('query', () => {
const s = query({
foo: 'ふぅ',
bar: 'b a r',
baz: undefined
});
assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r');
});
});

View File

@@ -0,0 +1,83 @@
/*
import * as assert from 'assert';
import { toDbReaction } from '../src/misc/reaction-lib';
describe('toDbReaction', async () => {
it('既存の文字列リアクションはそのまま', async () => {
assert.strictEqual(await toDbReaction('like'), 'like');
});
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
assert.strictEqual(await toDbReaction('🍮'), '🍮');
});
it('プリン以外の既存のリアクションは文字列化する like', async () => {
assert.strictEqual(await toDbReaction('👍'), 'like');
});
it('プリン以外の既存のリアクションは文字列化する love', async () => {
assert.strictEqual(await toDbReaction('❤️'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
assert.strictEqual(await toDbReaction('❤'), 'love');
});
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
assert.strictEqual(await toDbReaction('😆'), 'laugh');
});
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
});
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
assert.strictEqual(await toDbReaction('😮'), 'surprise');
});
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
});
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
assert.strictEqual(await toDbReaction('💢'), 'angry');
});
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
assert.strictEqual(await toDbReaction('😥'), 'confused');
});
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
assert.strictEqual(await toDbReaction('😇'), 'rip');
});
it('それ以外はUnicodeのまま', async () => {
assert.strictEqual(await toDbReaction('🍅'), '🍅');
});
it('異体字セレクタ除去', async () => {
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
});
it('異体字セレクタ除去 必要なし', async () => {
assert.strictEqual(await toDbReaction('㊗'), '㊗');
});
it('fallback - undefined', async () => {
assert.strictEqual(await toDbReaction(undefined), 'like');
});
it('fallback - null', async () => {
assert.strictEqual(await toDbReaction(null), 'like');
});
it('fallback - empty', async () => {
assert.strictEqual(await toDbReaction(''), 'like');
});
it('fallback - unknown', async () => {
assert.strictEqual(await toDbReaction('unknown'), 'like');
});
});
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@@ -0,0 +1,904 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils';
import { Following } from '../src/models/entities/following';
describe('Streaming', () => {
let p: childProcess.ChildProcess;
let Followings: any;
beforeEach(async () => {
p = await startServer();
const connection = await initTestDb(true);
Followings = connection.getRepository(Following);
});
afterEach(async () => {
await shutdownServer(p);
});
const follow = async (follower: any, followee: any) => {
await Followings.save({
id: 'a',
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
followerHost: follower.host,
followerInbox: null,
followerSharedInbox: null,
followeeHost: followee.host,
followeeInbox: null,
followeeSharedInbox: null
});
};
it('mention event', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const ws = await connectStream(bob, 'main', ({ type, body }) => {
if (type == 'mention') {
assert.deepStrictEqual(body.userId, alice.id);
ws.close();
done();
}
});
post(alice, {
text: 'foo @bob bar'
});
}));
it('renote event', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const bobNote = await post(bob, {
text: 'foo'
});
const ws = await connectStream(bob, 'main', ({ type, body }) => {
if (type == 'renote') {
assert.deepStrictEqual(body.renoteId, bobNote.id);
ws.close();
done();
}
});
post(alice, {
renoteId: bobNote.id
});
}));
describe('Home Timeline', () => {
it('自分の投稿が流れる', () => new Promise(async done => {
const post = {
text: 'foo'
};
const me = await signup();
const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.text, post.text);
ws.close();
done();
}
});
request('/notes/create', post, me);
}));
it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
post(bob, {
text: 'foo'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
assert.deepStrictEqual(body.text, 'foo');
ws.close();
done();
}
});
// Bob が Alice 宛てのダイレクト投稿
post(bob, {
text: 'foo',
visibility: 'specified',
visibleUserIds: [alice.id]
});
}));
it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const carol = await signup({ username: 'carol' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
let fired = false;
const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// Bob が Carol 宛てのダイレクト投稿
post(bob, {
text: 'foo',
visibility: 'specified',
visibleUserIds: [carol.id]
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
});
describe('Local Timeline', () => {
it('自分の投稿が流れる', () => new Promise(async done => {
const me = await signup();
const ws = await connectStream(me, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, me.id);
ws.close();
done();
}
});
post(me, {
text: 'foo'
});
}));
it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('リモートユーザーの投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob', host: 'example.com' });
let fired = false;
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
post(bob, {
text: 'foo'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob', host: 'example.com' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
let fired = false;
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
post(bob, {
text: 'foo'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('ホーム指定の投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// ホーム指定
post(bob, {
text: 'foo',
visibility: 'home'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしているローカルユーザーのダイレクト投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
let fired = false;
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// Bob が Alice 宛てのダイレクト投稿
post(bob, {
text: 'foo',
visibility: 'specified',
visibleUserIds: [alice.id]
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// フォロワー宛て投稿
post(bob, {
text: 'foo',
visibility: 'followers'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
});
describe('Hybrid Timeline', () => {
it('自分の投稿が流れる', () => new Promise(async done => {
const me = await signup();
const ws = await connectStream(me, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, me.id);
ws.close();
done();
}
});
post(me, {
text: 'foo'
});
}));
it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob', host: 'example.com' });
// Alice が Bob をフォロー
await follow(alice, bob);
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob', host: 'example.com' });
let fired = false;
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
post(bob, {
text: 'foo'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
assert.deepStrictEqual(body.text, 'foo');
ws.close();
done();
}
});
// Bob が Alice 宛てのダイレクト投稿
post(bob, {
text: 'foo',
visibility: 'specified',
visibleUserIds: [alice.id]
});
}));
it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// Alice が Bob をフォロー
await request('/following/create', {
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
assert.deepStrictEqual(body.text, 'foo');
ws.close();
done();
}
});
// ホーム投稿
post(bob, {
text: 'foo',
visibility: 'home'
});
}));
it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// ホーム投稿
post(bob, {
text: 'foo',
visibility: 'home'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// フォロワー宛て投稿
post(bob, {
text: 'foo',
visibility: 'followers'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
});
describe('Global Timeline', () => {
it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob', host: 'example.com' });
const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
});
post(bob, {
text: 'foo'
});
}));
it('ホーム投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
let fired = false;
const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
});
// ホーム投稿
post(bob, {
text: 'foo',
visibility: 'home'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
});
describe('UserList Timeline', () => {
it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// リスト作成
const list = await request('/users/lists/create', {
name: 'my list'
}, alice).then(x => x.body);
// Alice が Bob をリスイン
await request('/users/lists/push', {
listId: list.id,
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
ws.close();
done();
}
}, {
listId: list.id
});
post(bob, {
text: 'foo'
});
}));
it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// リスト作成
const list = await request('/users/lists/create', {
name: 'my list'
}, alice).then(x => x.body);
let fired = false;
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
}, {
listId: list.id
});
post(bob, {
text: 'foo'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
// #4471
it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// リスト作成
const list = await request('/users/lists/create', {
name: 'my list'
}, alice).then(x => x.body);
// Alice が Bob をリスイン
await request('/users/lists/push', {
listId: list.id,
userId: bob.id
}, alice);
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.userId, bob.id);
assert.deepStrictEqual(body.text, 'foo');
ws.close();
done();
}
}, {
listId: list.id
});
// Bob が Alice 宛てのダイレクト投稿
post(bob, {
text: 'foo',
visibility: 'specified',
visibleUserIds: [alice.id]
});
}));
// #4335
it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
const alice = await signup({ username: 'alice' });
const bob = await signup({ username: 'bob' });
// リスト作成
const list = await request('/users/lists/create', {
name: 'my list'
}, alice).then(x => x.body);
// Alice が Bob をリスイン
await request('/users/lists/push', {
listId: list.id,
userId: bob.id
}, alice);
let fired = false;
const ws = await connectStream(alice, 'userList', ({ type, body }) => {
if (type == 'note') {
fired = true;
}
}, {
listId: list.id
});
// フォロワー宛て投稿
post(bob, {
text: 'foo',
visibility: 'followers'
});
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 3000);
}));
});
describe('Hashtag Timeline', () => {
it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
const me = await signup();
const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
if (type == 'note') {
assert.deepStrictEqual(body.text, '#foo');
ws.close();
done();
}
}, {
q: [
['foo']
]
});
post(me, {
text: '#foo'
});
}));
it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
const me = await signup();
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
}
}, {
q: [
['foo', 'bar']
]
});
post(me, {
text: '#foo'
});
post(me, {
text: '#bar'
});
post(me, {
text: '#foo #bar'
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
ws.close();
done();
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
const me = await signup();
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
if (body.text === '#piyo') piyoCount++;
}
}, {
q: [
['foo'],
['bar']
]
});
post(me, {
text: '#foo'
});
post(me, {
text: '#bar'
});
post(me, {
text: '#foo #bar'
});
post(me, {
text: '#piyo'
});
setTimeout(() => {
assert.strictEqual(fooCount, 1);
assert.strictEqual(barCount, 1);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 0);
ws.close();
done();
}, 3000);
}));
it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
const me = await signup();
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
let waaaCount = 0;
const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
if (type == 'note') {
if (body.text === '#foo') fooCount++;
if (body.text === '#bar') barCount++;
if (body.text === '#foo #bar') fooBarCount++;
if (body.text === '#piyo') piyoCount++;
if (body.text === '#waaa') waaaCount++;
}
}, {
q: [
['foo', 'bar'],
['piyo']
]
});
post(me, {
text: '#foo'
});
post(me, {
text: '#bar'
});
post(me, {
text: '#foo #bar'
});
post(me, {
text: '#piyo'
});
post(me, {
text: '#waaa'
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 1);
assert.strictEqual(waaaCount, 0);
ws.close();
done();
}, 3000);
}));
});
});

View File

@@ -0,0 +1,103 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils';
describe('Note thread mute', () => {
let p: childProcess.ChildProcess;
let alice: any;
let bob: any;
let carol: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
});
after(async () => {
await shutdownServer(p);
});
it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await request('/notes/mentions', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false);
}));
it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const res = await request('/i', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
const bobNote = await post(bob, { text: '@alice @carol root note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
let fired = false;
const ws = await connectStream(alice, 'main', async ({ type, body }) => {
if (type === 'unreadMention') {
if (body === bobNote.id) return;
fired = true;
}
});
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
const res = await request('/i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
}));
});

View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"allowJs": true,
"noEmitOnError": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"sourceMap": true,
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"removeComments": false,
"noLib": false,
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
"baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
},
"typeRoots": [
"../node_modules/@types",
"../src/@types"
],
"lib": [
"esnext"
]
},
"compileOnSave": false,
"include": [
"./**/*.ts"
]
}

View File

@@ -0,0 +1,61 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils';
describe('users/notes', () => {
let p: childProcess.ChildProcess;
let alice: any;
let jpgNote: any;
let pngNote: any;
let jpgPngNote: any;
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg');
const png = await uploadFile(alice, __dirname + '/resources/Lenna.png');
jpgNote = await post(alice, {
fileIds: [jpg.id]
});
pngNote = await post(alice, {
fileIds: [png.id]
});
jpgPngNote = await post(alice, {
fileIds: [jpg.id, png.id]
});
});
after(async() => {
await shutdownServer(p);
});
it('ファイルタイプ指定 (jpg)', async(async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg']
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 2);
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
}));
it('ファイルタイプ指定 (jpg or png)', async(async () => {
const res = await request('/users/notes', {
userId: alice.id,
fileType: ['image/jpeg', 'image/png']
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.length, 3);
assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
}));
});

View File

@@ -0,0 +1,212 @@
import * as fs from 'fs';
import * as WebSocket from 'ws';
import * as misskey from 'misskey-js';
import fetch from 'node-fetch';
const FormData = require('form-data');
import * as childProcess from 'child_process';
import * as http from 'http';
import loadConfig from '../src/config/load';
import { SIGKILL } from 'constants';
import { createConnection, getConnection } from 'typeorm';
import { entities } from '../src/db/postgre';
const config = loadConfig();
export const port = config.port;
export const async = (fn: Function) => (done: Function) => {
fn().then(() => {
done();
}, (err: Error) => {
done(err);
});
};
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
const auth = me ? {
i: me.token
} : {};
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign(auth, params))
});
const status = res.status;
const body = res.status !== 204 ? await res.json().catch() : null;
return {
body, status
};
};
export const signup = async (params?: any): Promise<any> => {
const q = Object.assign({
username: 'test',
password: 'test'
}, params);
const res = await request('/signup', q);
return res.body;
};
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = Object.assign({
text: 'test'
}, params);
const res = await request('/notes/create', q, user);
return res.body ? res.body.createdNote : null;
};
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
await request('/notes/reactions/create', {
noteId: note.id,
reaction: reaction
}, user);
};
export const uploadFile = (user: any, path?: string): Promise<any> => {
const formData = new FormData();
formData.append('i', user.token);
formData.append('file', fs.createReadStream(path || __dirname + '/resources/Lenna.png'));
return fetch(`http://localhost:${port}/api/drive/files/create`, {
method: 'post',
body: formData,
timeout: 30 * 1000,
}).then(res => {
if (!res.ok) {
throw `${res.status} ${res.statusText}`;
} else {
return res.json();
}
});
};
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
return new Promise((res, rej) => {
const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`);
ws.on('open', () => {
ws.on('message', data => {
const msg = JSON.parse(data.toString());
if (msg.type == 'channel' && msg.body.id == 'a') {
listener(msg.body);
} else if (msg.type == 'connected' && msg.body.id == 'a') {
res(ws);
}
});
ws.send(JSON.stringify({
type: 'connect',
body: {
channel: channel,
id: 'a',
pong: true,
params: params
}
}));
});
});
}
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
// node-fetchだと3xxを取れない
return await new Promise((resolve, reject) => {
const req = http.request(`http://localhost:${port}${path}`, {
headers: {
Accept: accept
}
}, res => {
if (res.statusCode! >= 400) {
reject(res);
} else {
resolve({
status: res.statusCode,
type: res.headers['content-type'],
location: res.headers.location,
});
}
});
req.end();
});
};
export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) {
return (done: (err?: Error) => any) => {
const p = childProcess.spawn('node', [__dirname + '/../index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH }
});
callbackSpawnedProcess(p);
p.on('message', message => {
if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e));
});
};
}
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
try {
const conn = await getConnection();
await conn.close();
} catch (e) {}
return await createConnection({
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
synchronize: true && !justBorrow,
dropSchema: true && !justBorrow,
entities: initEntities || entities
});
}
export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
rej('timeout to start');
}, timeout);
const p = childProcess.spawn('node', [__dirname + '/../built/index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH }
});
p.on('error', e => rej(e));
p.on('message', message => {
if (message === 'ok') {
clearTimeout(t);
res(p);
}
});
});
}
export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000) {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
res('force exit');
}, timeout);
p.once('exit', () => {
clearTimeout(t);
res('exited');
});
p.kill();
});
}

View File

@@ -400,11 +400,6 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe"
integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/express-serve-static-core@*":
version "4.17.5"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c"
@@ -430,15 +425,6 @@
dependencies:
"@types/node" "*"
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/glob@7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -778,14 +764,6 @@
dependencies:
"@types/node" "*"
"@types/rimraf@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/rsvp@^4.0.4":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.4.tgz#55e93e7054027f1ad4b4ebc1e60e59eb091e2d32"

View File

@@ -39,7 +39,6 @@
"@types/random-seed": "0.3.3",
"@types/rename": "1.0.4",
"@types/request-stats": "3.0.0",
"@types/rimraf": "3.0.2",
"@types/seedrandom": "2.4.28",
"@types/sinonjs__fake-timers": "6.0.4",
"@types/speakeasy": "2.0.6",
@@ -113,7 +112,6 @@
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"request-stats": "3.0.0",
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.43.4",
@@ -133,8 +131,6 @@
"ts-node": "10.4.0",
"tsc-alias": "1.3.10",
"tsconfig-paths": "3.11.0",
"tslint": "6.1.3",
"tslint-sonarts": "1.9.0",
"twemoji-parser": "13.1.0",
"typescript": "4.4.4",
"uuid": "8.3.2",

View File

@@ -186,14 +186,6 @@ export default defineComponent({
os.success();
},
setAsAvatar() {
os.updateAvatar(this.file);
},
setAsBanner() {
os.updateBanner(this.file);
},
addApp() {
alert('not implemented yet');
},

View File

@@ -1,37 +1,58 @@
import { App } from 'vue';
import mfm from './global/misskey-flavored-markdown.vue';
import a from './global/a.vue';
import acct from './global/acct.vue';
import avatar from './global/avatar.vue';
import emoji from './global/emoji.vue';
import userName from './global/user-name.vue';
import ellipsis from './global/ellipsis.vue';
import time from './global/time.vue';
import url from './global/url.vue';
import i18n from './global/i18n';
import loading from './global/loading.vue';
import error from './global/error.vue';
import ad from './global/ad.vue';
import header from './global/header.vue';
import spacer from './global/spacer.vue';
import stickyContainer from './global/sticky-container.vue';
import Mfm from './global/misskey-flavored-markdown.vue';
import MkA from './global/a.vue';
import MkAcct from './global/acct.vue';
import MkAvatar from './global/avatar.vue';
import MkEmoji from './global/emoji.vue';
import MkUserName from './global/user-name.vue';
import MkEllipsis from './global/ellipsis.vue';
import MkTime from './global/time.vue';
import MkUrl from './global/url.vue';
import I18n from './global/i18n';
import MkLoading from './global/loading.vue';
import MkError from './global/error.vue';
import MkAd from './global/ad.vue';
import MkHeader from './global/header.vue';
import MkSpacer from './global/spacer.vue';
import MkStickyContainer from './global/sticky-container.vue';
export default function(app: App) {
app.component('I18n', i18n);
app.component('Mfm', mfm);
app.component('MkA', a);
app.component('MkAcct', acct);
app.component('MkAvatar', avatar);
app.component('MkEmoji', emoji);
app.component('MkUserName', userName);
app.component('MkEllipsis', ellipsis);
app.component('MkTime', time);
app.component('MkUrl', url);
app.component('MkLoading', loading);
app.component('MkError', error);
app.component('MkAd', ad);
app.component('MkHeader', header);
app.component('MkSpacer', spacer);
app.component('MkStickyContainer', stickyContainer);
app.component('I18n', I18n);
app.component('Mfm', Mfm);
app.component('MkA', MkA);
app.component('MkAcct', MkAcct);
app.component('MkAvatar', MkAvatar);
app.component('MkEmoji', MkEmoji);
app.component('MkUserName', MkUserName);
app.component('MkEllipsis', MkEllipsis);
app.component('MkTime', MkTime);
app.component('MkUrl', MkUrl);
app.component('MkLoading', MkLoading);
app.component('MkError', MkError);
app.component('MkAd', MkAd);
app.component('MkHeader', MkHeader);
app.component('MkSpacer', MkSpacer);
app.component('MkStickyContainer', MkStickyContainer);
}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
I18n: typeof I18n;
Mfm: typeof Mfm;
MkA: typeof MkA;
MkAcct: typeof MkAcct;
MkAvatar: typeof MkAvatar;
MkEmoji: typeof MkEmoji;
MkUserName: typeof MkUserName;
MkEllipsis: typeof MkEllipsis;
MkTime: typeof MkTime;
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkAd: typeof MkAd;
MkHeader: typeof MkHeader;
MkSpacer: typeof MkSpacer;
MkStickyContainer: typeof MkStickyContainer;
}
}

View File

@@ -86,7 +86,6 @@
</div>
<footer class="footer">
<div class="info">
<span class="mobile" v-if="appearNote.viaMobile"><i class="fas fa-mobile-alt"></i></span>
<MkTime class="created-at" :time="appearNote.createdAt" mode="detail"/>
</div>
<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>

View File

@@ -8,7 +8,6 @@
<div class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></div>
<div class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></div>
<div class="info">
<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
<MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</MkA>
@@ -99,10 +98,6 @@ export default defineComponent({
margin-left: auto;
font-size: 0.9em;
> .mobile {
margin-right: 8px;
}
> .visibility {
margin-left: 8px;
}

View File

@@ -76,7 +76,6 @@ import { noteVisibilities } from 'misskey-js';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { isMobile } from '@/scripts/is-mobile';
import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
import { defaultStore } from '@/store';
@@ -648,7 +647,6 @@ export default defineComponent({
localOnly: this.localOnly,
visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: isMobile
};
if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {

View File

@@ -1,147 +0,0 @@
<template>
<div class="mk-users-dialog">
<div class="header">
<span>{{ title }}</span>
<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
</div>
<div class="users">
<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)">
<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true" :show-indicator="true"/>
<div class="body">
<MkUserName :user="extract ? extract(item) : item" class="name"/>
<MkAcct :user="extract ? extract(item) : item" class="acct"/>
</div>
</MkA>
</div>
<button class="more _button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
</button>
<p class="empty" v-if="empty">{{ $ts.noUsers }}</p>
<MkError v-if="error" @retry="init()"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import paging from '@/scripts/paging';
import { userPage } from '@/filters/user';
export default defineComponent({
mixins: [
paging({}),
],
props: {
title: {
required: true
},
pagination: {
required: true
},
extract: {
required: false
}
},
data() {
return {
};
},
methods: {
userPage
}
});
</script>
<style lang="scss" scoped>
.mk-users-dialog {
width: 350px;
height: 350px;
background: var(--panel);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
> .header {
display: flex;
flex-shrink: 0;
> button {
height: 58px;
width: 58px;
@media (max-width: 500px) {
height: 42px;
width: 42px;
}
}
> span {
flex: 1;
line-height: 58px;
padding-left: 32px;
font-weight: bold;
@media (max-width: 500px) {
line-height: 42px;
padding-left: 16px;
}
}
}
> .users {
flex: 1;
overflow: auto;
&:empty {
display: none;
}
> .user {
display: flex;
align-items: center;
font-size: 14px;
padding: 8px 32px;
@media (max-width: 500px) {
padding: 8px 16px;
}
> * {
pointer-events: none;
user-select: none;
}
> .avatar {
width: 45px;
height: 45px;
}
> .body {
padding: 0 8px;
overflow: hidden;
> .name {
display: block;
font-weight: bold;
}
> .acct {
opacity: 0.5;
}
}
}
}
> .empty {
text-align: center;
opacity: 0.5;
}
}
</style>

View File

@@ -125,7 +125,6 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkUsersDialog from '@/components/users-dialog.vue';
import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
@@ -201,44 +200,15 @@ export default defineComponent({
},
showFollowing() {
os.modal(MkUsersDialog, {
title: this.$ts.instanceFollowing,
pagination: {
endpoint: 'federation/following',
limit: 10,
params: {
host: this.instance.host
}
},
extract: item => item.follower
});
// TODO: ページ遷移
},
showFollowers() {
os.modal(MkUsersDialog, {
title: this.$ts.instanceFollowers,
pagination: {
endpoint: 'federation/followers',
limit: 10,
params: {
host: this.instance.host
}
},
extract: item => item.followee
});
// TODO: ページ遷移
},
showUsers() {
os.modal(MkUsersDialog, {
title: this.$ts.instanceUsers,
pagination: {
endpoint: 'federation/users',
limit: 10,
params: {
host: this.instance.host
}
}
});
// TODO: ページ遷移
},
bytes,

View File

@@ -8,7 +8,6 @@
<span class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></span>
<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
<MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</MkA>
@@ -96,10 +95,6 @@ export default defineComponent({
font-size: 0.9em;
opacity: 0.7;
> .mobile {
margin-right: 8px;
}
> .visibility {
margin-left: 8px;
}

View File

@@ -61,7 +61,6 @@ import { Autocomplete } from '@/scripts/autocomplete';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store';
import { isMobile } from '@/scripts/is-mobile';
import { throttle } from 'throttle-debounce';
export default defineComponent({
@@ -544,7 +543,6 @@ export default defineComponent({
localOnly: this.localOnly,
visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: isMobile
};
// plugin

View File

@@ -34,6 +34,7 @@
},
"compileOnSave": false,
"include": [
"./**/*.ts"
"./**/*.ts",
"./**/*.vue"
]
}

View File

@@ -662,14 +662,6 @@
dependencies:
"@types/node" "*"
"@types/rimraf@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/seedrandom@2.4.28":
version "2.4.28"
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
@@ -1696,11 +1688,6 @@ bufferutil@^4.0.1:
dependencies:
node-gyp-build "~3.7.0"
builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
cacheable-lookup@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
@@ -1767,7 +1754,7 @@ chalk@4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1:
chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1969,7 +1956,7 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.12.1, commander@^2.20.0:
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -3276,7 +3263,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@7.1.6, glob@^7.1.1, glob@^7.1.3:
glob@7.1.6, glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -3545,11 +3532,6 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immutable@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@@ -4371,7 +4353,7 @@ misskey-js@0.0.10:
eventemitter3 "^4.0.7"
reconnecting-websocket "^4.4.0"
mkdirp@0.x, mkdirp@^0.5.3, mkdirp@~0.5.1:
mkdirp@0.x, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -5476,7 +5458,7 @@ resolve@^1.15.1:
is-core-module "^2.2.0"
path-parse "^1.0.6"
resolve@^1.3.2, resolve@^1.9.0:
resolve@^1.9.0:
version "1.18.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
@@ -5628,11 +5610,6 @@ semver@6.x:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^5.3.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
version "7.3.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
@@ -6204,11 +6181,6 @@ tsconfig-paths@3.11.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
tslib@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^1.8.1, tslib@^1.9.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
@@ -6224,39 +6196,6 @@ tslib@~2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tslint-sonarts@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/tslint-sonarts/-/tslint-sonarts-1.9.0.tgz#feb593e92db328c0328b430b838adbe65d504de9"
integrity sha512-CJWt+IiYI8qggb2O/JPkS6CkC5DY1IcqRsm9EHJ+AxoWK70lvtP7jguochyNDMP2vIz/giGdWCfEM39x/I/Vnw==
dependencies:
immutable "^3.8.2"
tslint@6.1.3:
version "6.1.3"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==
dependencies:
"@babel/code-frame" "^7.0.0"
builtin-modules "^1.1.1"
chalk "^2.3.0"
commander "^2.12.1"
diff "^4.0.1"
glob "^7.1.1"
js-yaml "^3.13.1"
minimatch "^3.0.4"
mkdirp "^0.5.3"
resolve "^1.3.2"
semver "^5.3.0"
tslib "^1.13.0"
tsutils "^2.29.0"
tsutils@^2.29.0:
version "2.29.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
dependencies:
tslib "^1.8.1"
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"