Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
Ryu jongheon 2022-06-07 20:33:08 +09:00 committed by GitHub
commit 91a366fda5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 408 additions and 222 deletions

View file

@ -1 +1 @@
v18.0.0 v16.15.0

View file

@ -10,18 +10,17 @@ You should also include the user name that made the change.
--> -->
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### NOTE
- From this version, Node 18.0.0 or later is required.
### Improvements ### Improvements
- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina - Supports Unicode Emoji 14.0 @mei23
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina - プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina - プッシュ通知にクリックやactionを設定 #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina - ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- replaced webpack with Vite @tamaina - Server: always remove completed tasks of job queue @Johann150
- update dependencies @syuilo - Client: make emoji stand out more on reaction button @Johann150
- enhance: display URL of QR code for TOTP registration @syuilo - Client: display URL of QR code for TOTP registration @tamaina
- enhance: Supports Unicode Emoji 14.0 @mei23 - API: notifications/readは配列でも受け付けるように #7667 @tamaina
- API: ユーザー検索で、クエリがusernameの条件を満たす場合はusernameもLIKE検索するように @tamaina
- MFM: Allow speed changes in all animated MFMs @Johann150
- The theme color is now better validated. @Johann150 - The theme color is now better validated. @Johann150
Your own theme color may be unset if it was in an invalid format. Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt. Admins should check their instance settings if in doubt.
@ -30,20 +29,31 @@ You should also include the user name that made the change.
Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address.
### Bugfixes ### Bugfixes
- Client: fix settings page @tamaina - Server: keep file order of note attachement @Johann150
- Client: fix profile tabs @futchitwo - Server: fix caching @Johann150
- Server: await promises when following or unfollowing users @Johann150 - Server: await promises when following or unfollowing users @Johann150
- Client: fix abuse reports page to be able to show all reports @Johann150
- Federation: Add rel attribute to host-meta @mei23
- Client: fix profile picture height in mentions @tamaina
- MFM: more animated functions support `speed` parameter @futchitwo
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150 - Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
- Server: fix internal in-memory caching @Johann150 - Server: fix internal in-memory caching @Johann150
- Server: use correct order of attachments on notes @Johann150 - Server: use correct order of attachments on notes @Johann150
- Server: prevent crash when processing certain PNGs @syuilo - Server: prevent crash when processing certain PNGs @syuilo
- Server: Fix unable to generate video thumbnails @mei23 - Server: Fix unable to generate video thumbnails @mei23
- Server: Fix `Cannot find module` issue @mei23 - Server: Fix `Cannot find module` issue @mei23
- Federation: Add rel attribute to host-meta @mei23
- Federation: add id for activitypub follows @Johann150
- Federation: ensure resolver does not fetch local resources via HTTP(S) @Johann150
- Federation: correctly render empty note text @Johann150
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Federation: remove duplicate br tag/newline @Johann150
- Federation: add missing authorization checks @Johann150
- Client: fix profile picture height in mentions @tamaina
- Client: fix abuse reports page to be able to show all reports @Johann150
- Client: fix settings page @tamaina
- Client: fix profile tabs @futchitwo
- Client: fix popout URL @futchitwo
- Client: correctly handle MiAuth URLs with query string @sn0w
- Client: ノート詳細ページの新しいノートを表示する機能の動作が正しくなるように修正する @xianonn
- MFM: more animated functions support `speed` parameter @futchitwo
- MFM: limit large MFM @Johann150
## 12.110.1 (2022/04/23) ## 12.110.1 (2022/04/23)

View file

@ -71,15 +71,17 @@ For now, basically only @syuilo has the authority to merge PRs into develop beca
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor. However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
## Release ## Release
For now, basically only @syuilo has the authority to release Misskey.
However, in case of emergency, a release can be made at the discretion of a contributor.
### Release Instructions ### Release Instructions
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) 1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
2. follow the `master` branch to the `develop` branch. 2. Create a release PR.
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) - Into `master` from `develop` branch.
- The target branch must be `master` - The title must be in the format `Release: x.y.z`.
- The tag name must be the version - `x.y.z` is the new version you are trying to release.
- Assign about 2~3 reviewers.
3. The release PR is approved, merge it.
4. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
- The target branch must be `master`
- The tag name must be the version
## Localization (l10n) ## Localization (l10n)
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js" "loader=./test/loader.js"
], ],
"slow": 1000, "slow": 1000,
"timeout": 3000, "timeout": 10000,
"exit": true "exit": true
} }

View file

@ -101,7 +101,7 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"summaly": "2.5.0", "summaly": "2.5.1",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.11.15", "systeminformation": "5.11.15",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",

View file

@ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js'; import { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
import { dbLogger } from './logger.js'; import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -207,7 +208,15 @@ export const db = new DataSource({
migrations: ['../../migration/*.js'], migrations: ['../../migration/*.js'],
}); });
export async function initDb() { export async function initDb(force = false) {
if (force) {
if (db.isInitialized) {
await db.destroy();
}
await db.initialize();
return;
}
if (db.isInitialized) { if (db.isInitialized) {
// nop // nop
} else { } else {
@ -217,6 +226,7 @@ export async function initDb() {
export async function resetDb() { export async function resetDb() {
const reset = async () => { const reset = async () => {
await redisClient.FLUSHDB();
const tables = await db.query(`SELECT relname AS "table" const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema') WHERE nspname NOT IN ('pg_catalog', 'information_schema')

View file

@ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] { getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) { if (file.properties.orientation != null) {
const properties = structuredClone(file.properties); // TODO
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) { if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width]; [properties.width, properties.height] = [properties.height, properties.width];
} }

View file

@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/
import { UserPublickey } from '@/models/entities/user-publickey.js'; import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
const publicKeyCache = new Cache<UserPublickey | null>(Infinity); const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export type UriParseResult = {
/** wether the URI was generated by us */
local: true;
/** id in DB */
id: string;
/** hint of type, e.g. "notes", "users" */
type: string;
/** any remaining text after type and id, not including the slash after id. undefined if empty */
rest?: string;
} | {
/** wether the URI was generated by us */
local: false;
/** uri in DB */
uri: string;
};
export function parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
local: true,
type: matchLocal[1],
id: matchLocal[2],
rest: matchLocal[3],
};
} else {
return {
local: false,
uri,
};
}
}
export default class DbResolver { export default class DbResolver {
constructor() { constructor() {
} }
@ -21,60 +59,54 @@ export default class DbResolver {
* AP Note => Misskey Note in DB * AP Note => Misskey Note in DB
*/ */
public async getNoteFromApId(value: string | IObject): Promise<Note | null> { public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
const parsed = this.parseUri(value); const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await Notes.findOneBy({ return await Notes.findOneBy({
id: parsed.id, id: parsed.id,
}); });
} } else {
if (parsed.uri) {
return await Notes.findOneBy({ return await Notes.findOneBy({
uri: parsed.uri, uri: parsed.uri,
}); });
} }
return null;
} }
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> { public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
const parsed = this.parseUri(value); const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await MessagingMessages.findOneBy({ return await MessagingMessages.findOneBy({
id: parsed.id, id: parsed.id,
}); });
} } else {
if (parsed.uri) {
return await MessagingMessages.findOneBy({ return await MessagingMessages.findOneBy({
uri: parsed.uri, uri: parsed.uri,
}); });
} }
return null;
} }
/** /**
* AP Person => Misskey User in DB * AP Person => Misskey User in DB
*/ */
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> { public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
const parsed = this.parseUri(value); const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'users') return null;
if (parsed.id) {
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id, id: parsed.id,
}).then(x => x ?? undefined)) ?? null; }).then(x => x ?? undefined)) ?? null;
} } else {
if (parsed.uri) {
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})); }));
} }
return null;
} }
/** /**
@ -120,31 +152,4 @@ export default class DbResolver {
key, key,
}; };
} }
public parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
type: matchLocal[1],
id: matchLocal[2],
};
} else {
return {
uri,
};
}
}
} }
type UriParseResult = {
/** id in DB (local object only) */
id?: string;
/** uri in DB (remote object only) */
uri?: string;
/** hint of type (local object only, ex: notes, users) */
type?: string
};

View file

@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js';
import { toHtml } from '../../../mfm/to-html.js'; import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) { export default function(note: Note) {
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; if (!note.text) return '';
if (html == null) html = '<p>.</p>'; return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
return html;
} }

View file

@ -1,8 +1,20 @@
import config from '@/config/index.js'; import config from '@/config/index.js';
import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { Blocking } from '@/models/entities/blocking.js';
export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ /**
type: 'Block', * Renders a block into its ActivityPub representation.
actor: `${config.url}/users/${blocker.id}`, *
object: blockee.uri, * @param block The block to be rendered. The blockee relation must be loaded.
}); */
export function renderBlock(block: Blocking) {
if (block.blockee?.url == null) {
throw new Error('renderBlock: missing blockee uri');
}
return {
type: 'Block',
id: `${config.url}/blocks/${block.id}`,
actor: `${config.url}/users/${block.blockerId}`,
object: block.blockee.uri,
};
}

View file

@ -4,12 +4,11 @@ import { Users } from '@/models/index.js';
export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
const follow = { const follow = {
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow', type: 'Follow',
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
} as any; } as any;
if (requestId) follow.id = requestId;
return follow; return follow;
}; };

View file

@ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds); const files = await getPromisedFiles(note.fileIds);
const text = note.text; // text should never be undefined
const text = note.text ?? null;
let poll: Poll | null = null; let poll: Poll | null = null;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id }); poll = await Polls.findOneBy({ noteId: note.id });
} }
let apText = text; let apText = text ?? '';
if (apText == null) apText = '';
if (quote) { if (quote) {
apText += `\n\nRE: ${quote}`; apText += `\n\nRE: ${quote}`;

View file

@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js';
import { ILocalUser } from '@/models/entities/user.js'; import { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js'; import { getInstanceActor } from '@/services/instance-actor.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { extractDbHost } from '@/misc/convert-host.js'; import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
import { signedGet } from './request.js'; import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js';
import { parseUri } from './db-resolver.js';
import renderNote from '@/remote/activitypub/renderer/note.js';
import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
import renderQuestion from '@/remote/activitypub/renderer/question.js';
import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
@ -40,14 +49,25 @@ export default class Resolver {
return value; return value;
} }
if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
throw new Error(`cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) { if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
this.history.add(value); this.history.add(value);
const meta = await fetchMeta();
const host = extractDbHost(value); const host = extractDbHost(value);
if (isSelfHost(host)) {
return await this.resolveLocal(value);
}
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) { if (meta.blockedHosts.includes(host)) {
throw new Error('Instance is blocked'); throw new Error('Instance is blocked');
} }
@ -70,4 +90,44 @@ export default class Resolver {
return object; return object;
} }
private resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url);
if (!parsed.local) throw new Error('resolveLocal: not local');
switch (parsed.type) {
case 'notes':
return Notes.findOneByOrFail({ id: parsed.id })
.then(note => {
if (parsed.rest === 'activity') {
// this refers to the create activity and not the note itself
return renderActivity(renderCreate(renderNote(note)));
} else {
return renderNote(note);
}
});
case 'users':
return Users.findOneByOrFail({ id: parsed.id })
.then(user => renderPerson(user as ILocalUser));
case 'questions':
// Polls are indexed by the note they are attached to.
return Promise.all([
Notes.findOneByOrFail({ id: parsed.id }),
Polls.findOneByOrFail({ noteId: parsed.id }),
])
.then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll));
case 'likes':
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null })));
case 'follows':
// rest should be <followee id>
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
return Promise.all(
[parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id }))
)
.then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url)));
default:
throw new Error(`resolveLocal: type ${type} unhandled`);
}
}
} }

View file

@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
import { isSelfHost } from '@/misc/convert-host.js'; import { isSelfHost } from '@/misc/convert-host.js';
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
import { ILocalUser, User } from '@/models/entities/user.js'; import { ILocalUser, User } from '@/models/entities/user.js';
import { In, IsNull } from 'typeorm'; import { In, IsNull, Not } from 'typeorm';
import { renderLike } from '@/remote/activitypub/renderer/like.js'; import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { getUserKeypair } from '@/misc/keypair-store.js'; import { getUserKeypair } from '@/misc/keypair-store.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
// Init router // Init router
const router = new Router(); const router = new Router();
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
setResponseType(ctx); setResponseType(ctx);
}); });
// follow
router.get('/follows/:follower/:followee', async ctx => {
// This may be used before the follow is completed, so we do not
// check if the following exists.
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
export default router; export default router;

View file

@ -1,5 +1,5 @@
import { Signins, UserProfiles, Users } from '@/models/index.js';
import define from '../../define.js'; import define from '../../define.js';
import { Users } from '@/models/index.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -23,9 +23,12 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => { export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId }); const [user, profile] = await Promise.all([
Users.findOneBy({ id: ps.userId }),
UserProfiles.findOneBy({ userId: ps.userId })
]);
if (user == null) { if (user == null || profile == null) {
throw new Error('user not found'); throw new Error('user not found');
} }
@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => {
throw new Error('cannot show info of admin'); throw new Error('cannot show info of admin');
} }
if (!_me.isAdmin) {
return {
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
};
}
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
Object.keys(profile.integrations).forEach(integration => {
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
});
const signins = await Signins.findBy({ userId: user.id });
return { return {
...user, email: profile.email,
token: user.token != null ? '<MASKED>' : user.token, emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
signins,
}; };
}); });

View file

@ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import manifest from './manifest.json' assert { type: 'json' }; import manifest from './manifest.json' assert { type: 'json' };
export const manifestHandler = async (ctx: Koa.Context) => { export const manifestHandler = async (ctx: Koa.Context) => {
const res = structuredClone(manifest); // TODO
//const res = structuredClone(manifest);
const res = JSON.parse(JSON.stringify(manifest));
const instance = await fetchMeta(true); const instance = await fetchMeta(true);

View file

@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js';
import renderBlock from '@/remote/activitypub/renderer/block.js'; import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import { deliver } from '@/queue/index.js'; import { deliver } from '@/queue/index.js';
import renderReject from '@/remote/activitypub/renderer/reject.js'; import renderReject from '@/remote/activitypub/renderer/reject.js';
import { Blocking } from '@/models/entities/blocking.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
import { perUserFollowingChart } from '@/services/chart/index.js'; import { perUserFollowingChart } from '@/services/chart/index.js';
@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
removeFromList(blockee, blocker), removeFromList(blockee, blocker),
]); ]);
await Blockings.insert({ const blocking = {
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
blocker,
blockerId: blocker.id, blockerId: blocker.id,
blockee,
blockeeId: blockee.id, blockeeId: blockee.id,
}); } as Blocking;
await Blockings.insert(blocking);
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderBlock(blocker, blockee)); const content = renderActivity(renderBlock(blocking));
deliver(blocker, content, blockee.inbox); deliver(blocker, content, blockee.inbox);
} }
} }

View file

@ -1,5 +1,5 @@
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderBlock from '@/remote/activitypub/renderer/block.js'; import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js';
import { deliver } from '@/queue/index.js'; import { deliver } from '@/queue/index.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
return; return;
} }
// Since we already have the blocker and blockee, we do not need to fetch
// them in the query above and can just manually insert them here.
blocking.blocker = blocker;
blocking.blockee = blockee;
Blockings.delete(blocking.id); Blockings.delete(blocking.id);
// deliver if remote bloking // deliver if remote bloking
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
deliver(blocker, content, blockee.inbox); deliver(blocker, content, blockee.inbox);
} }
} }

View file

@ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
import { entity as ApRequestChart } from './charts/entities/ap-request.js'; import { entity as ApRequestChart } from './charts/entities/ap-request.js';
import { entity as TestChart } from './charts/entities/test.js';
import { entity as TestGroupedChart } from './charts/entities/test-grouped.js';
import { entity as TestUniqueChart } from './charts/entities/test-unique.js';
import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js';
export const entities = [ export const entities = [
FederationChart.hour, FederationChart.day, FederationChart.hour, FederationChart.day,
NotesChart.hour, NotesChart.day, NotesChart.hour, NotesChart.day,
@ -24,4 +29,11 @@ export const entities = [
PerUserFollowingChart.hour, PerUserFollowingChart.day, PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day, PerUserDriveChart.hour, PerUserDriveChart.day,
ApRequestChart.hour, ApRequestChart.day, ApRequestChart.hour, ApRequestChart.day,
...(process.env.NODE_ENV === 'test' ? [
TestChart.hour, TestChart.day,
TestGroupedChart.hour, TestGroupedChart.day,
TestUniqueChart.hour, TestUniqueChart.day,
TestIntersectionChart.hour, TestIntersectionChart.day,
] : []),
]; ];

View file

@ -1,4 +1,4 @@
import { createSystemUser } from './create-system-user.js'; import { IsNull } from 'typeorm';
import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js'; import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js';
import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js';
@ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { Relay } from '@/models/entities/relay.js'; import { Relay } from '@/models/entities/relay.js';
import { IsNull } from 'typeorm'; import { createSystemUser } from './create-system-user.js';
const ACTOR_USERNAME = 'relay.actor' as const; const ACTOR_USERNAME = 'relay.actor' as const;
@ -88,7 +88,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;
const copy = structuredClone(activity); // TODO
//const copy = structuredClone(activity);
const copy = JSON.parse(JSON.stringify(activity));
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await attachLdSignature(copy, user); const signed = await attachLdSignature(copy, user);

View file

@ -2,11 +2,13 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import rndstr from 'rndstr'; import rndstr from 'rndstr';
import { initDb } from '../src/db/postgre.js';
import { initTestDb } from './utils.js'; import { initTestDb } from './utils.js';
describe('ActivityPub', () => { describe('ActivityPub', () => {
before(async () => { before(async () => {
await initTestDb(); //await initTestDb();
await initDb();
}); });
describe('Parse minimum object', () => { describe('Parse minimum object', () => {

View file

@ -6,26 +6,17 @@ import TestChart from '../src/services/chart/charts/test.js';
import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; import TestGroupedChart from '../src/services/chart/charts/test-grouped.js';
import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; import TestUniqueChart from '../src/services/chart/charts/test-unique.js';
import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js';
import * as _TestChart from '../src/services/chart/charts/entities/test.js'; import { initDb } from '../src/db/postgre.js';
import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js';
import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js';
import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js';
import { async, initTestDb } from './utils.js';
describe('Chart', () => { describe('Chart', () => {
let testChart: TestChart; let testChart: TestChart;
let testGroupedChart: TestGroupedChart; let testGroupedChart: TestGroupedChart;
let testUniqueChart: TestUniqueChart; let testUniqueChart: TestUniqueChart;
let testIntersectionChart: TestIntersectionChart; let testIntersectionChart: TestIntersectionChart;
let clock: lolex.Clock; let clock: lolex.InstalledClock;
beforeEach(async(async () => { beforeEach(async () => {
await initTestDb(false, [ await initDb(true);
_TestChart.entity.hour, _TestChart.entity.day,
_TestGroupedChart.entity.hour, _TestGroupedChart.entity.day,
_TestUniqueChart.entity.hour, _TestUniqueChart.entity.day,
_TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day,
]);
testChart = new TestChart(); testChart = new TestChart();
testGroupedChart = new TestGroupedChart(); testGroupedChart = new TestGroupedChart();
@ -34,14 +25,15 @@ describe('Chart', () => {
clock = lolex.install({ clock = lolex.install({
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
shouldClearNativeTimers: true,
}); });
})); });
afterEach(async(async () => { afterEach(() => {
clock.uninstall(); clock.uninstall();
})); });
it('Can updates', async(async () => { it('Can updates', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -63,9 +55,9 @@ describe('Chart', () => {
total: [1, 0, 0], total: [1, 0, 0],
}, },
}); });
})); });
it('Can updates (dec)', async(async () => { it('Can updates (dec)', async () => {
await testChart.decrement(); await testChart.decrement();
await testChart.save(); await testChart.save();
@ -87,9 +79,9 @@ describe('Chart', () => {
total: [-1, 0, 0], total: [-1, 0, 0],
}, },
}); });
})); });
it('Empty chart', async(async () => { it('Empty chart', async () => {
const chartHours = await testChart.getChart('hour', 3, null); const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null); const chartDays = await testChart.getChart('day', 3, null);
@ -108,9 +100,9 @@ describe('Chart', () => {
total: [0, 0, 0], total: [0, 0, 0],
}, },
}); });
})); });
it('Can updates at multiple times at same time', async(async () => { it('Can updates at multiple times at same time', async () => {
await testChart.increment(); await testChart.increment();
await testChart.increment(); await testChart.increment();
await testChart.increment(); await testChart.increment();
@ -134,9 +126,9 @@ describe('Chart', () => {
total: [3, 0, 0], total: [3, 0, 0],
}, },
}); });
})); });
it('複数回saveされてもデータの更新は一度だけ', async(async () => { it('複数回saveされてもデータの更新は一度だけ', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
await testChart.save(); await testChart.save();
@ -160,9 +152,9 @@ describe('Chart', () => {
total: [1, 0, 0], total: [1, 0, 0],
}, },
}); });
})); });
it('Can updates at different times', async(async () => { it('Can updates at different times', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -189,11 +181,11 @@ describe('Chart', () => {
total: [2, 0, 0], total: [2, 0, 0],
}, },
}); });
})); });
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip // 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/* /*
it('Can updates at different times without save', async(async () => { it('Can updates at different times without save', async () => {
await testChart.increment(); await testChart.increment();
clock.tick('01:00:00'); clock.tick('01:00:00');
@ -219,10 +211,10 @@ describe('Chart', () => {
total: [2, 0, 0] total: [2, 0, 0]
}, },
}); });
})); });
*/ */
it('Can padding', async(async () => { it('Can padding', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -249,10 +241,10 @@ describe('Chart', () => {
total: [2, 0, 0], total: [2, 0, 0],
}, },
}); });
})); });
// 要求された範囲にログがひとつもない場合でもパディングできる // 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async(async () => { it('Can padding from past range', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -276,11 +268,11 @@ describe('Chart', () => {
total: [1, 0, 0], total: [1, 0, 0],
}, },
}); });
})); });
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190 // Issue #3190
it('Can padding from past range 2', async(async () => { it('Can padding from past range 2', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -307,9 +299,9 @@ describe('Chart', () => {
total: [2, 0, 0], total: [2, 0, 0],
}, },
}); });
})); });
it('Can specify offset', async(async () => { it('Can specify offset', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -336,9 +328,9 @@ describe('Chart', () => {
total: [2, 0, 0], total: [2, 0, 0],
}, },
}); });
})); });
it('Can specify offset (floor time)', async(async () => { it('Can specify offset (floor time)', async () => {
clock.tick('00:30:00'); clock.tick('00:30:00');
await testChart.increment(); await testChart.increment();
@ -367,10 +359,10 @@ describe('Chart', () => {
total: [2, 0, 0], total: [2, 0, 0],
}, },
}); });
})); });
describe('Grouped', () => { describe('Grouped', () => {
it('Can updates', async(async () => { it('Can updates', async () => {
await testGroupedChart.increment('alice'); await testGroupedChart.increment('alice');
await testGroupedChart.save(); await testGroupedChart.save();
@ -410,11 +402,11 @@ describe('Chart', () => {
total: [0, 0, 0], total: [0, 0, 0],
}, },
}); });
})); });
}); });
describe('Unique increment', () => { describe('Unique increment', () => {
it('Can updates', async(async () => { it('Can updates', async () => {
await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice'); await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob'); await testUniqueChart.uniqueIncrement('bob');
@ -430,10 +422,10 @@ describe('Chart', () => {
assert.deepStrictEqual(chartDays, { assert.deepStrictEqual(chartDays, {
foo: [2, 0, 0], foo: [2, 0, 0],
}); });
})); });
describe('Intersection', () => { describe('Intersection', () => {
it('条件が満たされていない場合はカウントされない', async(async () => { it('条件が満たされていない場合はカウントされない', async () => {
await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob'); await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
@ -453,9 +445,9 @@ describe('Chart', () => {
b: [1, 0, 0], b: [1, 0, 0],
aAndB: [0, 0, 0], aAndB: [0, 0, 0],
}); });
})); });
it('条件が満たされている場合にカウントされる', async(async () => { it('条件が満たされている場合にカウントされる', async () => {
await testIntersectionChart.addA('alice'); await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob'); await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('carol');
@ -476,12 +468,12 @@ describe('Chart', () => {
b: [2, 0, 0], b: [2, 0, 0],
aAndB: [1, 0, 0], aAndB: [1, 0, 0],
}); });
})); });
}); });
}); });
describe('Resync', () => { describe('Resync', () => {
it('Can resync', async(async () => { it('Can resync', async () => {
testChart.total = 1; testChart.total = 1;
await testChart.resync(); await testChart.resync();
@ -504,9 +496,9 @@ describe('Chart', () => {
total: [1, 0, 0], total: [1, 0, 0],
}, },
}); });
})); });
it('Can resync (2)', async(async () => { it('Can resync (2)', async () => {
await testChart.increment(); await testChart.increment();
await testChart.save(); await testChart.save();
@ -534,6 +526,6 @@ describe('Chart', () => {
total: [100, 0, 0], total: [100, 0, 0],
}, },
}); });
})); });
}); });
}); });

View file

@ -6520,16 +6520,17 @@ style-loader@3.3.1:
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
summaly@2.5.0: summaly@2.5.1:
version "2.5.0" version "2.5.1"
resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea" resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.1.tgz#742fe6631987f84ad2e95d2b0f7902ec57e0f6b3"
integrity sha512-IzvO2s7yj/PUyH42qWjVjSPpIiPlgTRWGh33t4cIZKOqPQJ2INo7e83hXhHFr4hXTb3JRcIdCuM1ELjlrujiUQ== integrity sha512-WWvl7rLs3wm61Xc2JqgTbSuqtIOmGqKte+rkbnxe6ISy4089lQ+7F2ajooQNee6PWHl9kZ27SDd1ZMoL3/6R4A==
dependencies: dependencies:
cheerio "0.22.0" cheerio "0.22.0"
debug "4.3.3" debug "4.3.3"
escape-regexp "0.0.1" escape-regexp "0.0.1"
got "11.5.1" got "11.5.1"
html-entities "2.3.2" html-entities "2.3.2"
iconv-lite "0.6.3"
jschardet "3.0.0" jschardet "3.0.0"
koa "2.13.4" koa "2.13.4"
private-ip "2.3.3" private-ip "2.3.3"

View file

@ -39,8 +39,8 @@ export default defineComponent({
inject: { inject: {
sideViewHook: { sideViewHook: {
default: null default: null,
} },
}, },
provide() { provide() {
@ -94,31 +94,31 @@ export default defineComponent({
}, { }, {
icon: 'fas fa-expand-alt', icon: 'fas fa-expand-alt',
text: this.$ts.showInPage, text: this.$ts.showInPage,
action: this.expand action: this.expand,
}, this.sideViewHook ? { }, this.sideViewHook ? {
icon: 'fas fa-columns', icon: 'fas fa-columns',
text: this.$ts.openInSideView, text: this.$ts.openInSideView,
action: () => { action: () => {
this.sideViewHook(this.path); this.sideViewHook(this.path);
this.$refs.window.close(); this.$refs.window.close();
} },
} : undefined, { } : undefined, {
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
text: this.$ts.popout, text: this.$ts.popout,
action: this.popout action: this.popout,
}, null, { }, null, {
icon: 'fas fa-external-link-alt', icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab, text: this.$ts.openInNewTab,
action: () => { action: () => {
window.open(this.url, '_blank'); window.open(this.url, '_blank');
this.$refs.window.close(); this.$refs.window.close();
} },
}, { }, {
icon: 'fas fa-link', icon: 'fas fa-link',
text: this.$ts.copyLink, text: this.$ts.copyLink,
action: () => { action: () => {
copyToClipboard(this.url); copyToClipboard(this.url);
} },
}]; }];
}, },
}, },
@ -155,7 +155,7 @@ export default defineComponent({
onContextmenu(ev: MouseEvent) { onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev); os.contextMenu(this.contextmenu, ev);
} },
}, },
}); });
</script> </script>

View file

@ -222,7 +222,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction reaction: reaction,
}); });
}, () => { }, () => {
focus(); focus();
@ -233,7 +233,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction; const oldReaction = note.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
os.api('notes/reactions/delete', { os.api('notes/reactions/delete', {
noteId: note.id noteId: note.id,
}); });
} }
@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void { function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard viaKeyboard,
}).then(focus); }).then(focus);
} }
@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true, danger: true,
action: () => { action: () => {
os.api('notes/delete', { os.api('notes/delete', {
noteId: note.id noteId: note.id,
}); });
isDeleted.value = true; isDeleted.value = true;
} },
}], renoteTime.value, { }], renoteTime.value, {
viaKeyboard: viaKeyboard viaKeyboard: viaKeyboard,
}); });
} }
@ -288,14 +288,14 @@ function blur() {
os.api('notes/children', { os.api('notes/children', {
noteId: appearNote.id, noteId: appearNote.id,
limit: 30 limit: 30,
}).then(res => { }).then(res => {
replies.value = res; replies.value = res;
}); });
if (appearNote.replyId) { if (appearNote.replyId) {
os.api('notes/conversation', { os.api('notes/conversation', {
noteId: appearNote.replyId noteId: appearNote.replyId,
}).then(res => { }).then(res => {
conversation.value = res.reverse(); conversation.value = res.reverse();
}); });

View file

@ -210,7 +210,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', { os.api('notes/reactions/create', {
noteId: appearNote.id, noteId: appearNote.id,
reaction: reaction reaction: reaction,
}); });
}, () => { }, () => {
focus(); focus();
@ -221,7 +221,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction; const oldReaction = note.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
os.api('notes/reactions/delete', { os.api('notes/reactions/delete', {
noteId: note.id noteId: note.id,
}); });
} }
@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void { function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard viaKeyboard,
}).then(focus); }).then(focus);
} }
@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true, danger: true,
action: () => { action: () => {
os.api('notes/delete', { os.api('notes/delete', {
noteId: note.id noteId: note.id,
}); });
isDeleted.value = true; isDeleted.value = true;
} },
}], renoteTime.value, { }], renoteTime.value, {
viaKeyboard: viaKeyboard viaKeyboard: viaKeyboard,
}); });
} }
@ -284,7 +284,7 @@ function focusAfter() {
function readPromo() { function readPromo() {
os.api('promo/read', { os.api('promo/read', {
noteId: appearNote.id noteId: appearNote.id,
}); });
isDeleted.value = true; isDeleted.value = true;
} }

View file

@ -1,5 +1,6 @@
<template> <template>
<XModalWindow ref="dialog" <XModalWindow
ref="dialog"
:width="400" :width="400"
:height="450" :height="450"
:with-ok-button="true" :with-ok-button="true"
@ -28,18 +29,18 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue'; import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue'; import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import { notificationTypes } from 'misskey-js'; import XModalWindow from '@/components/ui/modal-window.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
XModalWindow, XModalWindow,
MkSwitch, MkSwitch,
MkInfo, MkInfo,
MkButton MkButton,
}, },
props: { props: {
@ -53,7 +54,7 @@ export default defineComponent({
type: Boolean, type: Boolean,
required: false, required: false,
default: true, default: true,
} },
}, },
emits: ['done', 'closed'], emits: ['done', 'closed'],
@ -93,7 +94,7 @@ export default defineComponent({
for (const type in this.typesMap) { for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true; this.typesMap[type as typeof notificationTypes[number]] = true;
} }
} },
} },
}); });
</script> </script>

View file

@ -16,7 +16,8 @@
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i> <i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i> <i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon v-else-if="notification.type === 'reaction'" <XReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef" ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis" :custom-emojis="notification.note.emojis"
@ -74,10 +75,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'; import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue'; import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue'; import MkFollowButton from './follow-button.vue';
import XReactionTooltip from './reaction-tooltip.vue'; import XReactionTooltip from './reaction-tooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({ export default defineComponent({
components: { components: {
XReactionIcon, MkFollowButton XReactionIcon, MkFollowButton,
}, },
props: { props: {
@ -116,7 +117,7 @@ export default defineComponent({
const readObserver = new IntersectionObserver((entries, observer) => { const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return; if (!entries.some(entry => entry.isIntersecting)) return;
stream.send('readNotification', { stream.send('readNotification', {
id: props.notification.id id: props.notification.id,
}); });
observer.disconnect(); observer.disconnect();
}); });

View file

@ -19,8 +19,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue'; import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import MkPagination from '@/components/ui/pagination.vue'; import MkPagination, { Paging } from '@/components/ui/pagination.vue';
import { Paging } from '@/components/ui/pagination.vue';
import XNotification from '@/components/notification.vue'; import XNotification from '@/components/notification.vue';
import XList from '@/components/date-separated-list.vue'; import XList from '@/components/date-separated-list.vue';
import XNote from '@/components/note.vue'; import XNote from '@/components/note.vue';
@ -49,14 +48,14 @@ const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') { if (isMuted || document.visibilityState === 'visible') {
stream.send('readNotification', { stream.send('readNotification', {
id: notification.id id: notification.id,
}); });
} }
if (!isMuted) { if (!isMuted) {
pagingComponent.value.prepend({ pagingComponent.value.prepend({
...notification, ...notification,
isRead: document.visibilityState === 'visible' isRead: document.visibilityState === 'visible',
}); });
} }
}; };

View file

@ -12,7 +12,7 @@ export default defineComponent({
props: { props: {
value: { value: {
type: Number, type: Number,
required: true required: true,
}, },
}, },
@ -26,7 +26,7 @@ export default defineComponent({
isZero, isZero,
number, number,
}; };
} },
}); });
</script> </script>

View file

@ -1,5 +1,6 @@
<template> <template>
<div ref="itemsEl" v-hotkey="keymap" <div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt" class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@ -162,6 +163,15 @@ function focusDown() {
position: relative; position: relative;
} }
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger { &.danger {
color: #ff2a2a; color: #ff2a2a;
@ -191,15 +201,6 @@ function focusDown() {
} }
} }
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&:not(:active):focus-visible { &:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset; box-shadow: 0 0 0 2px var(--focus) inset;
} }

View file

@ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { login } from '@/account'; import { login } from '@/account';
import { appendQuery, query } from '@/scripts/url';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -82,7 +83,9 @@ export default defineComponent({
this.state = 'accepted'; this.state = 'accepted';
if (this.callback) { if (this.callback) {
location.href = `${this.callback}?session=${this.session}`; location.href = appendQuery(this.callback, query({
session: this.session
}));
} }
}, },
deny() { deny() {

View file

@ -54,6 +54,9 @@
<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
</FormSection> </FormSection>
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user"> <MkObjectView tall :value="user">
</MkObjectView> </MkObjectView>
</div> </div>