Compare commits

..

60 commits

Author SHA1 Message Date
9f6be8d557
server: refactor meta caching
This removes the "caching" that re-fetches the instance meta information
from the database every 10 seconds.
2022-11-14 22:12:32 +01:00
9d9b2da6cc
fix parameter for cache fetcher 2022-11-13 20:31:24 +01:00
d1ec058d5c
server: refactor Cache to hold fetcher as attribute
Instead of having to pass the fetcher every time you want to fetch
something, the fetcher is stored in an attribute of the Cache.
2022-11-13 19:39:30 +01:00
131c12a30b
server: refactor prefetchEmojis
Exiting earlier might slightly improve performance.
2022-11-13 18:24:15 +01:00
8d6476af2a
server: remove localUserByIdCache
The same data is stored in userByIdCache. Whether a user is local or not
can easily be determined from the cached object.
2022-11-13 18:03:22 +01:00
57299f0df6
server: simplify caching for instance actor 2022-11-13 17:14:33 +01:00
b0489abd7f
translate japanese comments 2022-11-13 13:47:22 +01:00
26f1b66c6a
client: update API error dialog to error refactoring 2022-11-13 12:59:45 +01:00
1d877e97f0
client: fix maxlength for profile description
Changelog: Fixed
2022-11-13 11:58:11 +01:00
0571a0843c
client: improve suspend toggle 2022-11-13 01:12:05 +01:00
56033c26f0
service worker: remove dead code 2022-11-12 22:36:03 +01:00
80af8a143e
service worker: don't trigger "push notifications have been updated"
closes FoundKeyGang/FoundKey#121

Changelog: Fixed
2022-11-12 22:35:37 +01:00
a3468491a7
fix import 2022-11-12 18:51:57 +01:00
486be564e8
server: improve comments 2022-11-12 17:39:36 +01:00
c49f529ccb
server: use DeliverManager for user deletion 2022-11-12 15:23:49 +01:00
8979e779da
server: optimise follower inboxes query
Use the distinct query thingy so we don't have to make the Set work
so hard. This is also uniform code with the "everyone" above so should
hopefully be easier to understand.
2022-11-12 15:09:50 +01:00
Volpeon
b1bb5b28c5
client: remove wrong content type header 2022-11-12 09:43:24 +01:00
f3c38ad5c8
server: only add unique cascade-delete notes 2022-11-11 18:08:57 +01:00
899b01a031
remove unnecessary checks
These checks were made obsolete by commit
6df2f7c55c.
2022-11-11 18:07:49 +01:00
a27a29b371
server: redirect browsers to human readable page
Also added/translated more comments.
2022-11-11 17:54:11 +01:00
66a9d27ab1
server: increase user description length to 2048
Changelog: Changed
2022-11-11 12:28:57 +01:00
ed14fe8e79
client: remove hostname from signup & signin form
Long hostnames can obscure the username being entered. And the hostname
should already be known to the user anyway or they can find out by
looking at the current URL.

fixes <FoundKeyGang/FoundKey#231>

Changelog: Changed
2022-11-11 12:20:48 +01:00
d411ea6281
backend: make removeAds migration plain JS 2022-11-10 12:56:39 -05:00
5d23aa9e69
translate some comments to english 2022-11-10 00:36:39 +01:00
5b61941e4c
server: skip instances that proclaimed themself dead via HTTP 410
Changelog: Fixed
2022-11-10 00:23:30 +01:00
ca90cedba0
server: reduce dead instance detection to 7 days 2022-11-09 18:47:28 +01:00
2496b385ce
fix login
This is a fixup commit to b2c800e654.
2022-11-08 21:59:13 +01:00
54075789cd
server: remove content type bodge
Now that the client should send the proper content type, this should not be
necessary any more.
2022-11-08 20:57:38 +01:00
b2c800e654
client: properly set content-type header 2022-11-08 20:57:09 +01:00
5713f329ca
client: remove unnecessary ref 2022-11-08 20:57:08 +01:00
609312bb82
server: refactor errors in signin endpoint 2022-11-08 20:57:08 +01:00
7939d130aa backend: update sharp to 0.31.2
Changelog: Fixed
Fixes: FoundKeyGang/FoundKey#226
2022-11-08 01:16:55 -05:00
489eea0c67
server: improve API validation for creating apps
Resolves a FIXME comment.
2022-11-05 10:43:34 +01:00
6f65326b32
chore: synchronize code and database schema 2022-11-03 21:50:55 +01:00
408c5c3c65
improve description of generating migrations 2022-11-03 21:50:37 +01:00
e79d7879c6 docs/migrating: Make yarn instructions version-agnostic
This means we don't have to update the yarn version here in case we update the version of Yarn used.
2022-11-02 22:58:02 +00:00
e8ecd71f8a backend: refactor server/nodeinfo.ts (#221)
This fixes a few type errors like removing `software.respository` in
NodeInfo 2.0 and updating `metadata.repositoryUrl` to not use the
now removed meta `repositoryUrl` field.

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: FoundKeyGang/FoundKey#221
2022-11-02 21:42:51 +00:00
0db0db9a87
backend: fix types in getRedisFamily 2022-10-31 18:39:05 -04:00
6df2f7c55c
server: refactor finding delete-cascaded notes
Remove the several filter functions in different places by filtering
directly in the database.

Instead of a QueryBuilder, use the plain find function.

Refactor a for loop awaiting several promises individually, use
Array.map and await Promise.all to make better use of promises.
2022-10-31 20:57:45 +01:00
ac240eb58d
server: translate/add comments 2022-10-31 20:57:18 +01:00
e27494cf3e
chore: Provide type for toggleReaction 2022-10-31 10:10:29 +01:00
d725f93d40
backend: Provide type for signedGet 2022-10-31 10:10:29 +01:00
6db9b76f46
Retouch types in server index 2022-10-31 10:10:29 +01:00
f50b04b015
Fix type errors in withPackedNote 2022-10-31 10:10:28 +01:00
3fe1f7e70e
Deal with withPackedNote(onNote) types in stream channels 2022-10-31 10:10:28 +01:00
eff9dbb5ee
Reassure typechecker about token in authenticate 2022-10-31 10:10:28 +01:00
fb80fd1fbd
Broaden type in authenticate as undefined is also nullable 2022-10-31 10:10:27 +01:00
2a33d0ac83
Fix type import in stream emitter typing 2022-10-31 10:10:27 +01:00
fb5f498641
Upgrade bull-board to unify misaligned types in its packages 2022-10-31 10:10:27 +01:00
23fbdfdf1f
Fix typos in syslog initialization 2022-10-31 10:10:26 +01:00
5b7a7794ab
backend: fix type of IEndpointMeta.errors
The errors array is supposed to be readonly.
2022-10-31 03:35:47 -04:00
bd0c06e2d0
server: fix RefereceError (again...) 2022-10-30 17:46:44 +01:00
c282ed7683
Narrow type of isPureRenote
As side effect of that, a non-null assertion can be removed.

Co-authored-by: Johann150 <johann.galle@protonmail.com>
2022-10-30 17:38:56 +01:00
47b2f619a6
client: fix follow button getting stuck processing
If a user on a remote instances changes their profile to manually accept
follow requests, this change may not immediately be federated. Because of
this, a user may get stuck seeing "processing".
2022-10-30 17:27:05 +01:00
240ad1cca6
server: fix ReferenceError
The super constructor has to be called before accessing this.
2022-10-30 16:22:12 +01:00
eb1ecd90e6
client: Add "follows you" pill to user profile popup
Changelog: Added
Reviewed-on: FoundKeyGang/FoundKey#217
2022-10-30 14:41:11 +01:00
14c7d2bf53
client: fix ternary statement
fixup for 4bfbe0dd96
2022-10-30 11:00:40 +01:00
4bfbe0dd96
client: refactor pagination.vue
This mostly involves deduplicating code and removing redudndant
statements.

Also translated all but one comment to English.
2022-10-29 23:09:35 +02:00
2aafe8fc9f
server: avoid adding suspended instances to deliver queue
This should reduce the performance hit when adding large numbers of
instances to the deliver queue by making the check for suspended and
dead instances a bulk operation.

Changelog: Changed
Reviewed-on: FoundKeyGang/FoundKey#215
2022-10-29 22:58:04 +02:00
7a64a3858d
fix erroneous quote 2022-10-28 23:49:30 +02:00
86 changed files with 839 additions and 765 deletions

View file

@ -297,8 +297,11 @@ PostgreSQL array indices **start at 1**.
When `IN` is performed on a column that may contain `NULL` values, use `OR` or similar to handle `NULL` values.
### creating migrations
In `packages/backend`, run:
First make changes to the entity files in `packages/backend/src/models/entities/`.
Then, in `packages/backend`, run:
```sh
yarn build
npx typeorm migration:generate -d ormconfig.js -o <migration name>
```

View file

@ -41,7 +41,7 @@ git merge tags/v13.0.0-preview2 --squash
```
## Making sure modern Yarn works
Foundkey uses Yarn 3.2.3 instead of 1.x. To make sure the `yarn` command will work going forward, run `corepack enable`.
Foundkey uses Modern Yarn instead of Classic (1.x). To make sure the `yarn` command will work going forward, run `corepack enable`.
## Rebuilding and running database migrations
This will be pretty much the same as a regular update of Misskey. Note that `yarn install` may take a while since dependency versions have been updated or removed and we use a newer version of Yarn.

View file

@ -190,7 +190,9 @@ charts: "Charts"
perHour: "Per Hour"
perDay: "Per Day"
stopActivityDelivery: "Stop sending activities"
stopActivityDeliveryDescription: "Local activities will not be sent to this instance. Receiving activities works as before."
blockThisInstance: "Block this instance"
blockThisInstanceDescription: "Local activites will not be sent to this instance. Activites from this instance will be discarded."
operations: "Operations"
software: "Software"
version: "Version"

View file

@ -1,5 +1,5 @@
export class removeAds1657570176749 {
name = 'removeAds1657570176749'
name = 'removeAds1657570176749';
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "ad"`);

View file

@ -1,53 +0,0 @@
export class noteVisibilityFunction1662132062000 {
name = 'noteVisibilityFunction1662132062000';
async up(queryRunner) {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION note_visible(note_id varchar, user_id varchar) RETURNS BOOLEAN
LANGUAGE SQL
STABLE
CALLED ON NULL INPUT
AS $$
SELECT CASE
WHEN note_id IS NULL THEN TRUE
WHEN NOT EXISTS (SELECT 1 FROM note WHERE id = note_id) THEN FALSE
WHEN user_id IS NULL THEN (
-- simplified check without logged in user
SELECT
visibility IN ('public', 'home')
-- check reply / renote recursively
AND note_visible("replyId", NULL)
AND note_visible("renoteId", NULL)
FROM note WHERE note.id = note_id
) ELSE (
SELECT
(
visibility IN ('public', 'home')
OR
user_id = "userId"
OR
user_id = ANY("visibleUserIds")
OR
user_id = ANY("mentions")
OR (
visibility = 'followers'
AND
EXISTS (
SELECT 1 FROM following WHERE "followeeId" = "userId" AND "followerId" = user_id
)
)
)
-- check reply / renote recursively
AND note_visible("replyId", user_id)
AND note_visible("renoteId", user_id)
FROM note WHERE note.id = note_id
)
END;
$$;
`);
}
async down(queryRunner) {
await queryRunner.query('DROP FUNCTION note_visible');
}
}

View file

@ -0,0 +1,44 @@
export class sync1667503570994 {
name = 'sync1667503570994'
async up(queryRunner) {
await Promise.all([
// the migration for renote mutes added the index to the wrong table
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`),
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`),
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`),
queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `),
queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `),
queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET NOT NULL`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET DEFAULT ''`),
queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `),
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
]);
}
async down(queryRunner) {
await Promise.all([
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`),
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`),
queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP NOT NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`),
queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`),
queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`),
queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `),
]);
}
}

View file

@ -15,8 +15,8 @@
"test": "npm run mocha"
},
"dependencies": {
"@bull-board/api": "^4.2.2",
"@bull-board/koa": "4.0.0",
"@bull-board/api": "^4.3.1",
"@bull-board/koa": "^4.3.1",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0",
@ -96,7 +96,7 @@
"rss-parser": "3.12.0",
"sanitize-html": "2.7.0",
"semver": "7.3.7",
"sharp": "0.30.7",
"sharp": "0.31.2",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",

View file

@ -10,7 +10,7 @@ function getRedisFamily(family?: string | number): number {
dual: 0,
};
if (typeof family === 'string' && family in familyMap) {
return familyMap[family];
return familyMap[family as keyof typeof familyMap];
} else if (typeof family === 'number' && Object.values(familyMap).includes(family)) {
return family;
}

View file

@ -62,22 +62,21 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
// ハッシュタグ
// hashtags
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt;
// メンション
// mentions
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する
// restore the host name part
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
//#endregion
} else if (part.length === 3) {
text += txt;
}
// その他
// other
} else {
const generateLink = () => {
if (!href && !txt) {

View file

@ -1,10 +1,12 @@
export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number;
public fetcher: (key: string | null) => Promise<T | undefined>;
constructor(lifetime: Cache<never>['lifetime']) {
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
this.cache = new Map();
this.lifetime = lifetime;
this.fetcher = fetcher;
}
public set(key: string | null, value: T): void {
@ -17,10 +19,13 @@ export class Cache<T> {
public get(key: string | null): T | undefined {
const cached = this.cache.get(key);
if (cached == null) return undefined;
// discard if past the cache lifetime
if ((Date.now() - cached.date) > this.lifetime) {
this.cache.delete(key);
return undefined;
}
return cached.value;
}
@ -29,52 +34,22 @@ export class Cache<T> {
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
* If the value is cached, it is returned. Otherwise the fetcher is
* run to get the value. If the fetcher returns undefined, it is
* returned but not cached.
*/
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
public async fetch(key: string | null): Promise<T | undefined> {
const cached = this.get(key);
if (cached !== undefined) {
return cached;
} else {
const value = await this.fetcher(key);
// Cache MISS
const value = await fetcher();
this.set(key, value);
return value;
}
// don't cache undefined
if (value !== undefined)
this.set(key, value);
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
return value;
}
// Cache MISS
const value = await fetcher();
if (value !== undefined) {
this.set(key, value);
}
return value;
}
}

View file

@ -3,22 +3,26 @@ import { Note } from '@/models/entities/note.js';
import { User } from '@/models/entities/user.js';
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
import * as Acct from '@/misc/acct.js';
import { MINUTE } from '@/const.js';
import { getFullApAccount } from './convert-host.js';
import { Packed } from './schema.js';
import { Cache } from './cache.js';
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
const blockingCache = new Cache<User['id'][]>(
5 * MINUTE,
(blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)),
);
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
// designation for users you follow, list users and groups is disabled for performance reasons
/**
* noteUserFollowers / antennaUserFollowing
* either noteUserFollowers or antennaUserFollowing must be specified
*/
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
if (note.visibility === 'specified') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
// skip if the antenna creator is blocked by the note author
const blockings = await blockingCache.fetch(noteUser.id);
if (blockings.some(blocking => blocking === antenna.userId)) return false;
if (note.visibility === 'followers') {

View file

@ -1,44 +1,44 @@
import push from 'web-push';
import { db } from '@/db/postgre.js';
import { Meta } from '@/models/entities/meta.js';
import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js';
let cache: Meta;
/**
* Performs the primitive database operation to set the server configuration
*/
export async function setMeta(meta: Meta): Promise<void> {
const unlock = await getFetchInstanceMetadataLock('localhost');
// try to mitigate older bugs where multiple meta entries may have been created
db.manager.clear(Meta);
db.manager.insert(Meta, meta);
unlock();
}
/**
* Performs the primitive database operation to fetch server configuration.
* Writes to `cache` instead of returning.
*/
async function getMeta(): Promise<void> {
const unlock = await getFetchInstanceMetadataLock('localhost');
// new IDs are prioritised because multiple records may have been created due to past bugs
cache = db.manager.findOne(Meta, {
order: {
id: 'DESC',
},
});
unlock();
}
export async function fetchMeta(noCache = false): Promise<Meta> {
if (!noCache && cache) return cache;
return await db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(Meta, {
order: {
id: 'DESC',
},
});
await getMeta();
const meta = metas[0];
if (meta) {
cache = meta;
return meta;
} else {
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
const saved = await transactionalEntityManager
.upsert(
Meta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
cache = saved;
return saved;
}
});
return cache;
}
setInterval(() => {
fetchMeta(true).then(meta => {
cache = meta;
});
}, 1000 * 10);

View file

@ -3,8 +3,11 @@ import { User } from '@/models/entities/user.js';
import { UserKeypair } from '@/models/entities/user-keypair.js';
import { Cache } from './cache.js';
const cache = new Cache<UserKeypair>(Infinity);
const cache = new Cache<UserKeypair>(
Infinity,
(userId) => UserKeypairs.findOneByOrFail({ userId }),
);
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId }));
return await cache.fetch(userId);
}

View file

@ -4,14 +4,27 @@ import { Emojis } from '@/models/index.js';
import { Emoji } from '@/models/entities/emoji.js';
import { Note } from '@/models/entities/note.js';
import { query } from '@/prelude/url.js';
import { HOUR } from '@/const.js';
import { Cache } from './cache.js';
import { isSelfHost, toPunyNullable } from './convert-host.js';
import { decodeReaction } from './reaction-lib.js';
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
/**
* composite cache key: `${host ?? ''}:${name}`
*/
const cache = new Cache<Emoji | null>(
12 * HOUR,
async (key) => {
const [host, name] = key.split(':');
return (await Emojis.findOneBy({
name,
host: host || IsNull(),
})) || null;
},
);
/**
*
* Information needed to attach in ActivityPub
*/
type PopulatedEmoji = {
name: string;
@ -36,28 +49,22 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const name = match[1];
// ホスト正規化
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
return { name, host };
}
/**
*
* @param emojiName (:, @. (decodeReactionで可能))
* @param noteUserHost
* @returns , nullは未マッチを意味する
* Resolve emoji information from ActivityPub attachment.
* @param emojiName custom emoji names attached to notes, user profiles or in rections. Colons should not be included. Localhost is denote by @. (see also `decodeReaction`)
* @param noteUserHost host that the content is from, to default to
* @returns emoji information. `null` means not found.
*/
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null;
const queryOrNull = async () => (await Emojis.findOneBy({
name,
host: host ?? IsNull(),
})) || null;
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
const emoji = await cache.fetch(`${host ?? ''}:${name}`);
if (emoji == null) return null;
@ -72,7 +79,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
}
/**
* (, )
* Retrieve list of emojis from the cache. Uncached emoji are dropped.
*/
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
@ -103,11 +110,20 @@ export function aggregateNoteEmojis(notes: Note[]) {
}
/**
*
* Query list of emojis in bulk and add them to the cache.
*/
export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
const notCachedEmojis = emojis.filter(emoji => {
// check if the cache has this emoji
return cache.get(`${emoji.host ?? ''}:${emoji.name}`) == null;
});
// check if there even are any uncached emoji to handle
if (notCachedEmojis.length === 0) return;
// query all uncached emoji
const emojisQuery: any[] = [];
// group by hosts to try to reduce query size
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
emojisQuery.push({
@ -115,11 +131,14 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
host: host ?? IsNull(),
});
}
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
await Emojis.find({
where: emojisQuery,
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}) : [];
for (const emoji of _emojis) {
cache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}).then(emojis => {
// store all emojis into the cache
emojis.forEach(emoji => {
cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji);
});
});
}

View file

@ -1,5 +1,5 @@
import { Note } from '@/models/entities/note.js';
export function isPureRenote(note: Note): boolean {
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
}

View file

@ -0,0 +1,55 @@
import { Brackets } from 'typeorm';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Instances } from '@/models/index.js';
import { Instance } from '@/models/entities/instance.js';
import { DAY } from '@/const.js';
// Threshold from last contact after which an instance will be considered
// "dead" and should no longer get activities delivered to it.
const deadThreshold = 7 * DAY;
/**
* Returns the subset of hosts which should be skipped.
*
* @param hosts array of punycoded instance hosts
* @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter)
*/
export async function skippedInstances(hosts: Array<Instace['host']>): Array<Instance['host']> {
// first check for blocked instances since that info may already be in memory
const { blockedHosts } = await fetchMeta();
const skipped = hosts.filter(host => blockedHosts.includes(host));
// if possible return early and skip accessing the database
if (skipped.length === hosts.length) return hosts;
const deadTime = new Date(Date.now() - deadThreshold);
return skipped.concat(
await Instances.createQueryBuilder('instance')
.where('instance.host in (:...hosts)', {
// don't check hosts again that we already know are suspended
// also avoids adding duplicates to the list
hosts: hosts.filter(host => !skipped.includes(host)),
})
.andWhere(new Brackets(qb => { qb
.where('instance.isSuspended')
.orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime })
.orWhere('instance.latestStatus = 410');
}))
.select('host')
.getRawMany()
);
}
/**
* Returns whether a specific host (punycoded) should be skipped.
* Convenience wrapper around skippedInstances which should only be used if there is a single host to check.
* If you have multiple hosts, consider using skippedInstances instead to do a bulk check.
*
* @param host punycoded instance host
* @returns whether the given host should be skipped
*/
export async function shouldSkipInstance(host: Instance['host']): boolean {
const skipped = await skippedInstances([host]);
return skipped.length > 0;
}

View file

@ -62,7 +62,8 @@ export class DriveFile {
public size: number;
@Column('varchar', {
length: 512, nullable: true,
length: 2048,
nullable: true,
comment: 'The comment of the DriveFile.',
})
public comment: string | null;

View file

@ -7,7 +7,7 @@ export class Instance {
public id: string;
/**
*
* Date and time this instance was first seen.
*/
@Index()
@Column('timestamp with time zone', {
@ -16,7 +16,7 @@ export class Instance {
public caughtAt: Date;
/**
*
* Hostname
*/
@Index({ unique: true })
@Column('varchar', {
@ -26,7 +26,7 @@ export class Instance {
public host: string;
/**
*
* Number of users on this instance.
*/
@Column('integer', {
default: 0,
@ -35,7 +35,7 @@ export class Instance {
public usersCount: number;
/**
* 稿
* Number of notes on this instance.
*/
@Column('integer', {
default: 0,
@ -44,7 +44,7 @@ export class Instance {
public notesCount: number;
/**
*
* Number of local users who are followed by users from this instance.
*/
@Column('integer', {
default: 0,
@ -52,7 +52,7 @@ export class Instance {
public followingCount: number;
/**
*
* Number of users from this instance who are followed by local users.
*/
@Column('integer', {
default: 0,
@ -60,7 +60,7 @@ export class Instance {
public followersCount: number;
/**
*
* Timestamp of the latest outgoing HTTP request.
*/
@Column('timestamp with time zone', {
nullable: true,
@ -68,7 +68,7 @@ export class Instance {
public latestRequestSentAt: Date | null;
/**
* HTTPステータスコード
* HTTP status code that was received for the last outgoing HTTP request.
*/
@Column('integer', {
nullable: true,
@ -76,7 +76,7 @@ export class Instance {
public latestStatus: number | null;
/**
*
* Timestamp of the latest incoming HTTP request.
*/
@Column('timestamp with time zone', {
nullable: true,
@ -84,13 +84,13 @@ export class Instance {
public latestRequestReceivedAt: Date | null;
/**
*
* Timestamp of last communication with this instance (incoming or outgoing).
*/
@Column('timestamp with time zone')
public lastCommunicatedAt: Date;
/**
*
* Whether this instance seems unresponsive.
*/
@Column('boolean', {
default: false,
@ -98,7 +98,7 @@ export class Instance {
public isNotResponding: boolean;
/**
*
* Whether sending activities to this instance has been suspended.
*/
@Index()
@Column('boolean', {

View file

@ -77,7 +77,7 @@ async function populateMyReaction(note: Note, meId: User['id'], _hint_?: {
export const NoteRepository = db.getRepository(Note).extend({
async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// This code must always be synchronized with the `note_visible` SQL function.
// This code must always be synchronized with the checks in generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {

View file

@ -6,13 +6,16 @@ import { Packed } from '@/misc/schema.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
import { populateEmojis } from '@/misc/populate-emojis.js';
import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { db } from '@/db/postgre.js';
import { Instance } from '../entities/instance.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
const userInstanceCache = new Cache<Instance | null>(
3 * HOUR,
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
);
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
@ -27,7 +30,7 @@ const ajv = new Ajv();
const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
const passwordSchema = { type: 'string', minLength: 1 } as const;
const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 2048 } as const;
const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
@ -309,17 +312,15 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
instance: user.host ? userInstanceCache.fetch(user.host,
() => Instances.findOneBy({ host: user.host! }),
v => v != null,
).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
instance: !user.host ? undefined : userInstanceCache.fetch(user.host)
.then(instance => !instance ? undefined : {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
}),
emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),

View file

@ -6,39 +6,20 @@ import Logger from '@/services/logger.js';
import { Instances } from '@/models/index.js';
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { toPuny } from '@/misc/convert-host.js';
import { Cache } from '@/misc/cache.js';
import { Instance } from '@/models/entities/instance.js';
import { StatusError } from '@/misc/fetch.js';
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
import { DeliverJobData } from '@/queue/types.js';
import { LessThan } from 'typeorm';
import { DAY } from '@/const.js';
const logger = new Logger('deliver');
let latest: string | null = null;
const deadThreshold = 30 * DAY;
export default async (job: Bull.Job<DeliverJobData>) => {
const { host } = new URL(job.data.to);
const puny = toPuny(host);
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(puny)) {
return 'skip (blocked)';
}
const deadTime = new Date(Date.now() - deadThreshold);
const isSuspendedOrDead = await Instances.countBy([
{ host: puny, isSuspended: true },
{ host: puny, lastCommunicatedAt: LessThan(deadTime) },
]);
if (isSuspendedOrDead) {
return 'skip (suspended or dead)';
}
if (await shouldSkipInstance(puny)) return 'skip';
try {
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
@ -81,8 +62,8 @@ export default async (job: Bull.Job<DeliverJobData>) => {
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく
// A client error means that something is wrong with the request we are making,
// which means that retrying it makes no sense.
return `${res.statusCode} ${res.statusMessage}`;
}

View file

@ -10,8 +10,14 @@ 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 publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyCache = new Cache<UserPublickey>(
Infinity,
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
);
const publicKeyByUserIdCache = new Cache<UserPublickey>(
Infinity,
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
);
export type UriParseResult = {
/** wether the URI was generated by us */
@ -99,13 +105,9 @@ export default class DbResolver {
if (parsed.local) {
if (parsed.type !== 'users') return null;
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
return await userByIdCache.fetch(parsed.id) ?? null;
} else {
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri,
}));
return await uriPersonCache.fetch(parsed.uri) ?? null;
}
}
@ -116,20 +118,12 @@ export default class DbResolver {
user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOneBy({
keyId,
});
if (key == null) return null;
return key;
}, key => key != null);
const key = await publicKeyCache.fetch(keyId);
if (key == null) return null;
return {
user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser,
key,
};
}
@ -145,7 +139,7 @@ export default class DbResolver {
if (user == null) return null;
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
const key = await publicKeyByUserIdCache.fetch(user.id);
return {
user,

View file

@ -2,6 +2,7 @@ import { IsNull, Not } from 'typeorm';
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
import { Users, Followings } from '@/models/index.js';
import { deliver } from '@/queue/index.js';
import { skippedInstances } from '@/misc/skipped-instances.js';
//#region types
interface IRecipe {
@ -118,26 +119,18 @@ export default class DeliverManager {
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await Followings.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
const followers = await Followings.createQueryBuilder('followings')
// return either the shared inbox (if available) or the individual inbox
.select('COALESCE(followings.followerSharedInbox, followings.followerInbox)', 'inbox')
// so we don't have to make our inboxes Set work as hard
.distinct(true)
// ...for the specific actors followers
.where('followings.followeeId = :actorId', { actorId: this.actor.id })
// don't deliver to ourselves
.andWhere('followings.followerHost IS NOT NULL')
.getRawMany();
for (const following of followers) {
const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox);
}
followers.forEach(({ inbox }) => inboxes.add(inbox));
}
this.recipes.filter((recipe): recipe is IDirectRecipe =>
@ -150,8 +143,19 @@ export default class DeliverManager {
)
.forEach(recipe => inboxes.add(recipe.to.inbox!));
const instancesToSkip = await skippedInstances(
// get (unique) list of hosts
Array.from(new Set(
Array.from(inboxes)
.map(inbox => new URL(inbox).host)
))
);
// deliver
for (const inbox of inboxes) {
// skip instances as indicated
if (instancesToSkip.includes(new URL(inbox).host)) continue;
deliver(this.actor, this.activity, inbox);
}
}

View file

@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
* @param user http-signature user
* @param url URL to fetch
*/
export async function signedGet(url: string, user: { id: User['id'] }) {
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
const keypair = await getUserKeypair(user.id);
const req = createSignedGet({

View file

@ -23,8 +23,6 @@ import Featured from './activitypub/featured.js';
// Init router
const router = new Router();
//#region Routing
function inbox(ctx: Router.RouterContext) {
let signature;
@ -45,6 +43,8 @@ const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystr
function isActivityPubReq(ctx: Router.RouterContext) {
ctx.response.vary('Accept');
// if no accept header is supplied, koa returns the 1st, so html is used as a dummy
// i.e. activitypub requests must be explicit
const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
return typeof accepted === 'string' && !accepted.match(/html/);
}
@ -77,7 +77,7 @@ router.get('/notes/:note', async (ctx, next) => {
return;
}
// リモートだったらリダイレクト
// redirect if remote
if (note.userHost != null) {
if (note.uri == null || isSelfHost(note.userHost)) {
ctx.status = 500;
@ -94,6 +94,15 @@ router.get('/notes/:note', async (ctx, next) => {
// note activity
router.get('/notes/:note/activity', async ctx => {
if (!isActivityPubReq(ctx)) {
/*
Redirect to the human readable page. in this case using next is not possible,
since there is no human readable page explicitly for the activity.
*/
ctx.redirect(`/notes/${ctx.params.note}`);
return;
}
const note = await Notes.findOneBy({
id: ctx.params.note,
userHost: IsNull(),
@ -185,7 +194,6 @@ router.get('/@:user', async (ctx, next) => {
await userInfo(ctx, user);
});
//#endregion
// emoji
router.get('/emojis/:emoji', async ctx => {

View file

@ -3,10 +3,13 @@ import { Users, AccessTokens, Apps } from '@/models/index.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { Cache } from '@/misc/cache.js';
import { App } from '@/models/entities/app.js';
import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
import isNativeToken from './common/is-native-token.js';
const appCache = new Cache<App>(Infinity);
const appCache = new Cache<App>(
Infinity,
(id) => Apps.findOneByOrFail({ id }),
);
export class AuthenticationError extends Error {
constructor(message: string) {
@ -15,8 +18,8 @@ export class AuthenticationError extends Error {
}
}
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
let token: string | null = null;
export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
let maybeToken: string | null = null;
// check if there is an authorization header set
if (authorization != null) {
@ -27,19 +30,19 @@ export default async (authorization: string | null | undefined, bodyToken: strin
// check if OAuth 2.0 Bearer tokens are being used
// Authorization schemes are case insensitive
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
token = authorization.substring(7);
maybeToken = authorization.substring(7);
} else {
throw new AuthenticationError('unsupported authentication scheme');
}
} else if (bodyToken != null) {
token = bodyToken;
maybeToken = bodyToken;
} else {
return [null, null];
}
const token: string = maybeToken;
if (isNativeToken(token)) {
const user = await localUserByNativeTokenCache.fetch(token,
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
const user = await localUserByNativeTokenCache.fetch(token);
if (user == null) {
throw new AuthenticationError('unknown token');
@ -63,14 +66,13 @@ export default async (authorization: string | null | undefined, bodyToken: strin
lastUsedAt: new Date(),
});
const user = await localUserByIdCache.fetch(accessToken.userId,
() => Users.findOneBy({
id: accessToken.userId,
}) as Promise<ILocalUser>);
const user = await userByIdCache.fetch(accessToken.userId);
// can't authorize remote users
if (!Users.isLocalUser(user)) return [null, null];
if (accessToken.appId) {
const app = await appCache.fetch(accessToken.appId,
() => Apps.findOneByOrFail({ id: accessToken.appId! }));
const app = await appCache.fetch(accessToken.appId);
return [user, {
id: accessToken.id,

View file

@ -1,17 +1,42 @@
import { SelectQueryBuilder } from 'typeorm';
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { User } from '@/models/entities/user.js';
import { Note } from '@/models/entities/note.js';
import { Notes } from '@/models/index.js';
import { Followings } from '@/models/index.js';
export function visibilityQuery(q: SelectQueryBuilder<Note>, meId?: User['id'] | null = null): SelectQueryBuilder<Note> {
const superQuery = Notes.createQueryBuilder()
.from(() => q, 'note');
if (meId == null) {
superQuery.where('note_visible(note.id, null);');
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where("note.visibility = 'public'")
.orWhere("note.visibility = 'home'");
}));
} else {
superQuery.where('note_visible(note.id, :meId)', { meId });
}
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :meId');
return q;
q.andWhere(new Brackets(qb => { qb
// 公開投稿である
.where(new Brackets(qb => { qb
.where("note.visibility = 'public'")
.orWhere("note.visibility = 'home'");
}))
// または 自分自身
.orWhere('note.userId = :meId')
// または 自分宛て
.orWhere(':meId = ANY(note.visibleUserIds)')
.orWhere(':meId = ANY(note.mentions)')
.orWhere(new Brackets(qb => { qb
// または フォロワー宛ての投稿であり、
.where("note.visibility = 'followers'")
.andWhere(new Brackets(qb => { qb
// 自分がフォロワーである
.where(`note.userId IN (${ followingQuery.getQuery() })`)
// または 自分の投稿へのリプライ
.orWhere('note.replyUserId = :meId');
}));
}));
}));
q.setParameters({ meId: me.id });
}
}

View file

@ -2,7 +2,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
import { User } from '@/models/entities/user.js';
import { Note } from '@/models/entities/note.js';
import { Notes, Users } from '@/models/index.js';
import { visibilityQuery } from './generate-visibility-query.js';
import { generateVisibilityQuery } from './generate-visibility-query.js';
/**
* Get note for API processing, taking into account visibility.
@ -13,7 +13,9 @@ export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null)
id: noteId,
});
const note = await visibilityQuery(query, me).getOne();
generateVisibilityQuery(query, me);
const note = await query.getOne();
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');

View file

@ -622,7 +622,7 @@ export interface IEndpointMeta {
readonly tags?: ReadonlyArray<string>;
readonly errors?: Array<keyof errors>;
readonly errors?: ReadonlyArray<keyof typeof errors>;
readonly res?: Schema;

View file

@ -1,6 +1,5 @@
import { Meta } from '@/models/entities/meta.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { db } from '@/db/postgre.js';
import { fetchMeta, setMeta } from '@/misc/fetch-meta.js';
import define from '../../define.js';
export const meta = {
@ -375,20 +374,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro;
}
await db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(Meta, {
order: {
id: 'DESC',
},
});
const meta = metas[0];
if (meta) {
await transactionalEntityManager.update(Meta, meta.id, set);
} else {
await transactionalEntityManager.save(Meta, set);
}
const meta = await fetchMeta();
await setMeta({
...meta,
...set,
});
insertModerationLog(me, 'updateMeta');

View file

@ -1,7 +1,7 @@
import { readNote } from '@/services/note/read.js';
import { Antennas, Notes, AntennaNotes } from '@/models/index.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
import define from '../../define.js';
@ -65,10 +65,11 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
const notes = await visibilityQuery(query, user)
const notes = await query
.take(ps.limit)
.getMany();

View file

@ -2,6 +2,7 @@ import { Apps } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { unique } from '@/prelude/array.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { kinds } from '@/misc/api-permissions.js';
import define from '../../define.js';
export const meta = {
@ -21,10 +22,14 @@ export const paramDef = {
properties: {
name: { type: 'string' },
description: { type: 'string' },
permission: { type: 'array', uniqueItems: true, items: {
type: 'string',
// FIXME: add enum of possible permissions
} },
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
enum: kinds,
},
},
callbackUrl: { type: 'string', nullable: true },
},
required: ['name', 'description', 'permission'],

View file

@ -1,7 +1,7 @@
import { ClipNotes, Clips, Notes } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { ApiError } from '../../error.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -65,11 +65,12 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
if (user) {
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
}
const notes = await visibilityQuery(query, user)
const notes = await query
.take(ps.limit)
.getMany();

View file

@ -1,7 +1,7 @@
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -55,12 +55,13 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner');
generateVisibilityQuery(query, user);
if (user) {
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
}
const notes = await visibilityQuery(query, user).getMany();
const notes = await query.getMany();
return await Notes.packMany(notes, user, { detail: false });
});

View file

@ -5,7 +5,7 @@ import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
@ -84,6 +84,7 @@ export default define(meta, paramDef, async (ps, user) => {
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
@ -124,7 +125,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
//#endregion
const timeline = await visibilityQuery(query, user).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
activeUsersChart.read(user);

View file

@ -6,7 +6,7 @@ import define from '../../define.js';
import { ApiError } from '../../error.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -77,6 +77,7 @@ export default define(meta, paramDef, async (ps, user) => {
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
@ -102,7 +103,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
//#endregion
const timeline = await visibilityQuery(query, user).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
if (user) {

View file

@ -3,7 +3,7 @@ import { noteVisibilities } from 'foundkey-js';
import { readNote } from '@/services/note/read.js';
import { Notes, Followings } from '@/models/index.js';
import define from '../../define.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -63,6 +63,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteThreadQuery(query, user);
generateBlockedUserQuery(query, user);
@ -76,7 +77,7 @@ export default define(meta, paramDef, async (ps, user) => {
query.setParameters(followingQuery.getParameters());
}
const mentions = await visibilityQuery(query, user).take(ps.limit).getMany();
const mentions = await query.take(ps.limit).getMany();
readNote(user.id, mentions);

View file

@ -2,7 +2,7 @@ import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -57,10 +57,11 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
const renotes = await visibilityQuery(query, user).take(ps.limit).getMany();
const renotes = await query.take(ps.limit).getMany();
return await Notes.packMany(renotes, user);
});

View file

@ -1,7 +1,7 @@
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -48,10 +48,11 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
const timeline = await visibilityQuery(query, user).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
return await Notes.packMany(timeline, user);
});

View file

@ -5,7 +5,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
@ -80,6 +80,7 @@ export default define(meta, paramDef, async (ps, me) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
if (me) generateBlockedUserQuery(query, me);
@ -133,7 +134,7 @@ export default define(meta, paramDef, async (ps, me) => {
}
// Search notes
const notes = await visibilityQuery(query, me).take(ps.limit).getMany();
const notes = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, me);
});

View file

@ -4,7 +4,7 @@ import config from '@/config/index.js';
import es from '@/db/elasticsearch.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -68,10 +68,11 @@ export default define(meta, paramDef, async (ps, me) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
if (me) generateBlockedUserQuery(query, me);
const notes = await visibilityQuery(query, me).take(ps.limit).getMany();
const notes = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, me);
} else {

View file

@ -3,7 +3,7 @@ import { Notes, Followings } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
@ -82,6 +82,7 @@ export default define(meta, paramDef, async (ps, user) => {
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
@ -122,7 +123,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
//#endregion
const timeline = await visibilityQuery(query, user).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
activeUsersChart.read(user);

View file

@ -4,7 +4,7 @@ import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
export const meta = {
tags: ['notes', 'lists'],
@ -70,6 +70,8 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
generateVisibilityQuery(query, user);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.userId != :meId', { meId: user.id });
@ -105,7 +107,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
//#endregion
const timeline = await visibilityQuery(query, user).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
activeUsersChart.read(user);

View file

@ -4,7 +4,7 @@ import define from '../../define.js';
import { ApiError } from '../../error.js';
import { getUser } from '../../common/getters.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -69,6 +69,7 @@ export default define(meta, paramDef, async (ps, me) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, me);
if (me) {
generateMutedUserQuery(query, me);
generateBlockedUserQuery(query, me);
@ -109,7 +110,7 @@ export default define(meta, paramDef, async (ps, me) => {
//#endregion
const timeline = await visibilityQuery(query, me).take(ps.limit).getMany();
const timeline = await query.take(ps.limit).getMany();
return await Notes.packMany(timeline, me);
});

View file

@ -1,7 +1,7 @@
import { NoteReactions, UserProfiles } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { visibilityQuery } from '../../common/generate-visibility-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -50,7 +50,9 @@ export default define(meta, paramDef, async (ps, me) => {
.andWhere('reaction.userId = :userId', { userId: ps.userId })
.leftJoinAndSelect('reaction.note', 'note');
const reactions = await visibilityQuery(query, me)
generateVisibilityQuery(query, me);
const reactions = await query
.take(ps.limit)
.getMany();

View file

@ -9,15 +9,14 @@ export class ApiError extends Error {
info?: any | null,
) {
if (!(code in errors)) {
this.info = `Unknown error "${code}" occurred.`;
this.code = 'INTERNAL_ERROR';
} else {
this.info = info;
this.code = code;
info = `Unknown error "${code}" occurred.`;
code = 'INTERNAL_ERROR';
}
const { message, httpStatusCode } = errors[this.code];
const { message, httpStatusCode } = errors[code];
super(message);
this.code = code;
this.info = info;
this.message = message;
this.httpStatusCode = httpStatusCode;
}

View file

@ -32,10 +32,7 @@ app.use(async (ctx, next) => {
await next();
});
app.use(bodyParser({
// リクエストが multipart/form-data でない限りはJSONだと見なす
detectJSON: ctx => !ctx.is('multipart/form-data'),
}));
app.use(bodyParser());
// Init multer instance
const upload = multer({

View file

@ -39,7 +39,7 @@ export const httpCodes: Record<string, string> = {
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I'm a Teapot',
'418': 'I\'m a Teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Content',
'423': 'Locked',

View file

@ -11,48 +11,51 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import signin from '../common/signin.js';
import { verifyLogin, hash } from '../2fa.js';
import { limiter } from '../limiter.js';
import { ApiError } from '../error.js';
export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url);
ctx.set('Access-Control-Allow-Credentials', 'true');
const body = ctx.request.body as any;
const username = body['username'];
const password = body['password'];
const token = body['token'];
const { username, password, token } = body;
function error(status: number, error: { id: string }) {
ctx.status = status;
ctx.body = { error };
// taken from @server/api/api-handler.ts
function error (e: ApiError): void {
ctx.status = e.httpStatusCode;
if (e.httpStatusCode === 401) {
ctx.response.set('WWW-Authenticate', 'Bearer');
}
ctx.body = {
error: {
message: e!.message,
code: e!.code,
...(e!.info ? { info: e!.info } : {}),
endpoint: endpoint.name,
},
};
}
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip));
} catch (err) {
ctx.status = 429;
ctx.body = {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
error(new ApiError('RATE_LIMIT_EXCEEDED'));
return;
}
if (typeof username !== 'string') {
ctx.status = 400;
error(new ApiError('INVALID_PARAM', { param: 'username', reason: 'not a string' }));
return;
}
if (typeof password !== 'string') {
ctx.status = 400;
error(new ApiError('INVALID_PARAM', { param: 'password', reason: 'not a string' }));
return;
}
if (token != null && typeof token !== 'string') {
ctx.status = 400;
error(new ApiError('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' }));
return;
}
@ -63,16 +66,12 @@ export default async (ctx: Koa.Context) => {
}) as ILocalUser;
if (user == null) {
error(404, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
error(new ApiError('NO_SUCH_USER'));
return;
}
if (user.isSuspended) {
error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
error(new ApiError('SUSPENDED'));
return;
}
@ -81,7 +80,7 @@ export default async (ctx: Koa.Context) => {
// Compare password
const same = await bcrypt.compare(password, profile.password!);
async function fail(status?: number, failure?: { id: string }) {
async function fail(): void {
// Append signin history
await Signins.insert({
id: genId(),
@ -92,7 +91,7 @@ export default async (ctx: Koa.Context) => {
success: false,
});
error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
error(new ApiError('ACCESS_DENIED'));
}
if (!profile.twoFactorEnabled) {
@ -100,18 +99,14 @@ export default async (ctx: Koa.Context) => {
signin(ctx, user);
return;
} else {
await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
await fail();
return;
}
}
if (token) {
if (!same) {
await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
await fail();
return;
}
@ -126,16 +121,12 @@ export default async (ctx: Koa.Context) => {
signin(ctx, user);
return;
} else {
await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
await fail();
return;
}
} else if (body.credentialId) {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
await fail();
return;
}
@ -149,9 +140,7 @@ export default async (ctx: Koa.Context) => {
});
if (!challenge) {
await fail(403, {
id: '2715a88a-2125-4013-932f-aa6fe72792da',
});
await fail();
return;
}
@ -161,9 +150,7 @@ export default async (ctx: Koa.Context) => {
});
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
await fail(403, {
id: '2715a88a-2125-4013-932f-aa6fe72792da',
});
await fail();
return;
}
@ -177,9 +164,7 @@ export default async (ctx: Koa.Context) => {
});
if (!securityKey) {
await fail(403, {
id: '66269679-aeaf-4474-862b-eb761197e046',
});
await fail();
return;
}
@ -196,16 +181,12 @@ export default async (ctx: Koa.Context) => {
signin(ctx, user);
return;
} else {
await fail(403, {
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
});
await fail();
return;
}
} else {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
await fail();
return;
}
@ -214,9 +195,7 @@ export default async (ctx: Koa.Context) => {
});
if (keys.length === 0) {
await fail(403, {
id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4',
});
await fail();
return;
}

View file

@ -62,21 +62,21 @@ export default abstract class Channel {
});
}
protected withPackedNote(callback: (note: Packed<'Note'>) => void): (Note) => void {
protected withPackedNote(callback: (note: Packed<'Note'>) => Promise<void>): (note: Note) => Promise<void> {
return async (note: Note) => {
try {
// because `note` was previously JSON.stringify'ed, the fields that
// were objects before are now strings and have to be restored or
// removed from the object
note.createdAt = new Date(note.createdAt);
delete note.reply;
delete note.renote;
delete note.user;
delete note.channel;
note.reply = null;
note.renote = null;
note.user = null;
note.channel = null;
const packed = await Notes.pack(note, this.user, { detail: true });
callback(packed);
await callback(packed);
} catch (err) {
if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
// skip: note not visible to user

View file

@ -2,6 +2,7 @@ import { Users } from '@/models/index.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { User } from '@/models/entities/user.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import { StreamMessages } from '../types.js';
import Channel from '../channel.js';
@ -12,10 +13,11 @@ export default class extends Channel {
private channelId: string;
private typers: Record<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -27,7 +29,7 @@ export default class extends Channel {
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
if (note.channelId !== this.channelId) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する

View file

@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -25,7 +27,7 @@ export default class extends Channel {
this.subscriber.on('notesStream', this.onNote);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;

View file

@ -1,6 +1,7 @@
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
@ -8,10 +9,11 @@ export default class extends Channel {
public static shouldShare = false;
public static requireCredential = false;
private q: string[][];
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -23,7 +25,7 @@ export default class extends Channel {
this.subscriber.on('notesStream', this.onNote);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;

View file

@ -2,16 +2,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -19,7 +21,7 @@ export default class extends Channel {
this.subscriber.on('notesStream', this.onNote);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
if (note.channelId) {
if (!this.followingChannels.has(note.channelId)) return;
} else {

View file

@ -3,16 +3,18 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -23,7 +25,7 @@ export default class extends Channel {
this.subscriber.on('notesStream', this.onNote);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または

View file

@ -2,16 +2,18 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -24,7 +26,7 @@ export default class extends Channel {
this.subscriber.on('notesStream', this.onNote);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;

View file

@ -2,6 +2,7 @@ import { UserListJoinings, UserLists } from '@/models/index.js';
import { User } from '@/models/entities/user.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js';
import Channel from '../channel.js';
export default class extends Channel {
@ -11,11 +12,12 @@ export default class extends Channel {
private listId: string;
public listUsers: User['id'][] = [];
private listUsersClock: NodeJS.Timer;
private onNote: (note: Note) => Promise<void>;
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.updateListUsers = this.updateListUsers.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.onNote = this.withPackedNote(this.onPackedNote.bind(this));
}
public async init(params: any) {
@ -48,7 +50,7 @@ export default class extends Channel {
this.listUsers = users.map(x => x.userId);
}
private async onNote(note: Packed<'Note'>) {
private async onPackedNote(note: Packed<'Note'>): Promise<void> {
if (!this.listUsers.includes(note.userId)) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する

View file

@ -1,5 +1,4 @@
import { EventEmitter } from 'events';
import Emitter from 'strict-event-emitter-types';
import { Channel } from '@/models/entities/channel.js';
import { User } from '@/models/entities/user.js';
import { UserProfile } from '@/models/entities/user-profile.js';
@ -15,6 +14,7 @@ import { Signin } from '@/models/entities/signin.js';
import { Page } from '@/models/entities/page.js';
import { Packed } from '@/misc/schema.js';
import { Webhook } from '@/models/entities/webhook.js';
import type { StrictEventEmitter as Emitter } from 'strict-event-emitter-types';
//#region Stream type-body definitions
export interface InternalStreamTypes {

View file

@ -138,13 +138,13 @@ export const startServer = () => {
return server;
};
export default () => new Promise(resolve => {
export default (): Promise<void> => new Promise(resolve => {
const server = createServer();
initializeStreamingServer(server);
server.on('error', e => {
switch ((e as any).code) {
switch ((e as NodeJS.ErrnoException).code) {
case 'EACCES':
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
break;
@ -164,5 +164,5 @@ export default () => new Promise(resolve => {
}
});
server.listen(config.port, resolve);
server.listen(config.port, () => resolve());
});

View file

@ -3,7 +3,6 @@ import { IsNull, MoreThan } from 'typeorm';
import config from '@/config/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, Notes } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
const router = new Router();
@ -18,7 +17,33 @@ export const links = [{
href: config.url + nodeinfo2_0path,
}];
const nodeinfo2 = async () => {
const repository = 'https://akkoma.dev/FoundKeyGang/FoundKey';
type NodeInfo2Base = {
software: {
name: string;
version: string;
repository?: string; // Not used in NodeInfo 2.0; used in 2.1
};
protocols: string[];
services: {
inbound: string[];
outbound: string[];
};
openRegistrations: boolean;
usage: {
users: {
total: number;
activeHalfyear: number;
activeMonth: number;
};
localPosts: number;
localComments: number;
};
metadata: Record<string, any>;
};
const nodeinfo2 = async (): Promise<NodeInfo2Base> => {
const now = Date.now();
const [
meta,
@ -40,7 +65,7 @@ const nodeinfo2 = async () => {
software: {
name: 'foundkey',
version: config.version,
repository: 'https://akkoma.dev/FoundKeyGang/FoundKey',
repository,
},
protocols: ['activitypub'],
services: {
@ -62,7 +87,7 @@ const nodeinfo2 = async () => {
},
langs: meta.langs,
tosUrl: meta.ToSUrl,
repositoryUrl: meta.repositoryUrl,
repositoryUrl: repository,
feedbackUrl: 'ircs://irc.akkoma.dev/foundkey',
disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
@ -82,17 +107,15 @@ const nodeinfo2 = async () => {
};
};
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
router.get(nodeinfo2_1path, async ctx => {
const base = await cache.fetch(null, () => nodeinfo2());
const base = await nodeinfo2();
ctx.body = { version: '2.1', ...base };
ctx.set('Cache-Control', 'public, max-age=600');
});
router.get(nodeinfo2_0path, async ctx => {
const base = await cache.fetch(null, () => nodeinfo2());
const base = await nodeinfo2();
delete base.software.repository;

View file

@ -1,28 +1,20 @@
import { IsNull } from 'typeorm';
import { ILocalUser } from '@/models/entities/user.js';
import { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { createSystemUser } from './create-system-user.js';
const ACTOR_USERNAME = 'instance.actor' as const;
const cache = new Cache<ILocalUser>(Infinity);
let instanceActor = await Users.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
}) as ILocalUser | undefined;
export async function getInstanceActor(): Promise<ILocalUser> {
const cached = cache.get(null);
if (cached) return cached;
const user = await Users.findOneBy({
host: IsNull(),
username: ACTOR_USERNAME,
}) as ILocalUser | undefined;
if (user) {
cache.set(null, user);
return user;
if (instanceActor) {
return instanceActor;
} else {
const created = await createSystemUser(ACTOR_USERNAME) as ILocalUser;
cache.set(null, created);
instanceActor = await createSystemUser(ACTOR_USERNAME) as ILocalUser;
return created;
}
}

View file

@ -28,9 +28,9 @@ export default class Logger {
if (config.syslog) {
this.syslogClient = new SyslogPro.RFC5424({
applacationName: 'FoundKey',
applicationName: 'FoundKey',
timestamp: true,
encludeStructuredData: true,
includeStructuredData: true,
color: true,
extendedColor: true,
server: {

View file

@ -36,13 +36,22 @@ import { Cache } from '@/misc/cache.js';
import { UserProfile } from '@/models/entities/user-profile.js';
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
import { IActivity } from '@/remote/activitypub/type.js';
import { MINUTE } from '@/const.js';
import { updateHashtags } from '../update-hashtag.js';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
import { createNotification } from '../create-notification.js';
import { addNoteToAntenna } from '../add-note-to-antenna.js';
import { deliverToRelays } from '../relay.js';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(
5 * MINUTE,
() => UserProfiles.find({
where: {
enableWordMute: true,
},
select: ['userId', 'mutedWords'],
}),
);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -257,12 +266,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
incNotesCountOfUser(user);
// Word mute
mutedWordsCache.fetch(null, () => UserProfiles.find({
where: {
enableWordMute: true,
},
select: ['userId', 'mutedWords'],
})).then(us => {
mutedWordsCache.fetch(null).then(us => {
for (const u of us) {
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
if (shouldMute) {

View file

@ -17,14 +17,14 @@ import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js
import { deliverToRelays } from '../relay.js';
/**
* 稿
* @param user 稿
* @param note 稿
* Delete your note.
* @param user author
* @param note note to be deleted
*/
export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false): Promise<void> {
const deletedAt = new Date();
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
// If this is the only renote of this note by this user
if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
Notes.decrement({ id: note.renoteId }, 'score', 1);
@ -37,17 +37,13 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
if (!quiet) {
publishNoteStream(note.id, 'deleted', { deletedAt });
//#region ローカルの投稿なら削除アクティビティを配送
// deliver delete activity of note itself for local posts
if (Users.isLocalUser(user) && !note.localOnly) {
let renote: Note | null = null;
// if deleted note is renote
if (isPureRenote(note)) {
renote = await Notes.findOneBy({
// isPureRenote checks if note.renoteId is null already, so renoteId should be non-null.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: note.renoteId!,
});
renote = await Notes.findOneBy({ id: note.renoteId });
}
const content = renderActivity(renote
@ -57,17 +53,14 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
deliverToConcerned(user, note, content);
}
// also deliever delete activity to cascaded notes
const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
// also deliver delete activity to cascaded notes
const cascadingNotes = await findCascadingNotes(note);
for (const cascadingNote of cascadingNotes) {
if (!cascadingNote.user) continue;
if (!Users.isLocalUser(cascadingNote.user)) continue;
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
deliverToConcerned(cascadingNote.user, cascadingNote, content);
}
//#endregion
// 統計を更新
// update statistics
notesChart.update(note, false);
perUserNotesChart.update(user, note, false);
@ -85,26 +78,43 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
});
}
/**
* Search for notes that will be affected by ON CASCADE DELETE.
* However, only notes for which it is relevant to deliver delete activities are searched.
* This means only local notes that are not local-only are searched.
*/
async function findCascadingNotes(note: Note): Promise<Note[]> {
const cascadingNotes: Note[] = [];
const recursive = async (noteId: string): Promise<void> => {
const query = Notes.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
q.where('note.renoteId = :noteId', { noteId })
.andWhere('note.text IS NOT NULL');
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
for (const reply of replies) {
// FIXME: use note_replies SQL function? Unclear what to do with 2nd and 3rd parameter, maybe rewrite the function.
const replies = await Notes.find({
where: [{
replyId: noteId,
localOnly: false,
userHost: IsNull(),
}, {
renoteId: noteId,
text: Not(IsNull()),
localOnly: false,
userHost: IsNull(),
}],
relations: {
user: true,
},
});
await Promise.all(replies.map(reply => {
// only add unique notes
if (cascadingNotes.find((x) => x.id == reply.id) != null) return;
cascadingNotes.push(reply);
await recursive(reply.id);
}
return recursive(reply.id);
}));
};
await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
return cascadingNotes;
}
async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {

View file

@ -15,20 +15,21 @@ type pushNotificationsTypes = {
'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
};
// プッシュメッセージサーバーには文字数制限があるため、内容を削減します
// Reduce the content of the push message because of the character limit
function truncateNotification(notification: Packed<'Notification'>): any {
if (notification.note) {
return {
...notification,
note: {
...notification.note,
// textをgetNoteSummaryしたものに置き換える
// replace text with getNoteSummary
text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note),
cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
// unnecessary, since usually the user who is receiving the notification knows who they are
user: undefined as any,
},
};
}
@ -41,7 +42,7 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(u
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
// Register key pair information
push.setVapidDetails(config.url,
meta.swPublicKey,
meta.swPrivateKey);
@ -65,10 +66,6 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(u
}), {
proxy: config.proxy,
}).catch((err: any) => {
//swLogger.info(err.statusCode);
//swLogger.info(err.headers);
//swLogger.info(err.body);
if (err.statusCode === 410) {
SwSubscriptions.delete({
userId,

View file

@ -3,29 +3,27 @@ import { Instances } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { toPuny } from '@/misc/convert-host.js';
import { Cache } from '@/misc/cache.js';
import { HOUR } from '@/const.js';
const cache = new Cache<Instance>(1000 * 60 * 60);
const cache = new Cache<Instance>(
HOUR,
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
);
export async function registerOrFetchInstanceDoc(idnHost: string): Promise<Instance> {
const host = toPuny(idnHost);
const cached = cache.get(host);
const cached = cache.fetch(host);
if (cached) return cached;
const index = await Instances.findOneBy({ host });
// apparently a new instance
const i = await Instances.insert({
id: genId(),
host,
caughtAt: new Date(),
lastCommunicatedAt: new Date(),
}).then(x => Instances.findOneByOrFail(x.identifiers[0]));
if (index == null) {
const i = await Instances.insert({
id: genId(),
host,
caughtAt: new Date(),
lastCommunicatedAt: new Date(),
}).then(x => Instances.findOneByOrFail(x.identifiers[0]));
cache.set(host, i);
return i;
} else {
cache.set(host, index);
return index;
}
cache.set(host, i);
return i;
}

View file

@ -8,11 +8,21 @@ import { Users, Relays } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { Cache } from '@/misc/cache.js';
import { Relay } from '@/models/entities/relay.js';
import { MINUTE } from '@/const.js';
import { createSystemUser } from './create-system-user.js';
const ACTOR_USERNAME = 'relay.actor' as const;
const relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
/**
* There is only one cache key: null.
* A cache is only used here to have expiring storage.
*/
const relaysCache = new Cache<Relay[]>(
10 * MINUTE,
() => Relays.findBy({
status: 'accepted',
}),
);
export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOneBy({
@ -83,9 +93,7 @@ export async function relayRejected(id: string) {
export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) {
if (activity == null) return;
const relays = await relaysCache.fetch(null, () => Relays.findBy({
status: 'accepted',
}));
const relays = await relaysCache.fetch(null);
if (relays.length === 0) return;
// TODO

View file

@ -1,7 +1,7 @@
import { Not, IsNull } from 'typeorm';
import renderDelete from '@/remote/activitypub/renderer/delete.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { deliver } from '@/queue/index.js';
import DeliverManager from '@/remote/activitypub/deliver-manager.js';
import config from '@/config/index.js';
import { User } from '@/models/entities/user.js';
import { Users, Followings } from '@/models/index.js';
@ -11,27 +11,11 @@ export async function doPostSuspend(user: { id: User['id']; host: User['host'] }
publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (Users.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user));
const queue: string[] = [];
const followings = await Followings.find({
where: [
{ followerSharedInbox: Not(IsNull()) },
{ followeeSharedInbox: Not(IsNull()) },
],
select: ['followerSharedInbox', 'followeeSharedInbox'],
});
const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
for (const inbox of inboxes) {
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
}
for (const inbox of queue) {
deliver(user, content, inbox);
}
// deliver to all of known network
const dm = new DeliverManager(user, content);
dm.addEveryone();
await dm.execute();
}
}

View file

@ -3,10 +3,18 @@ import { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { subscriber } from '@/db/redis.js';
export const userByIdCache = new Cache<CacheableUser>(Infinity);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>(Infinity);
export const localUserByIdCache = new Cache<CacheableLocalUser>(Infinity);
export const uriPersonCache = new Cache<CacheableUser | null>(Infinity);
export const userByIdCache = new Cache<CacheableUser>(
Infinity,
(id) => Users.findOneBy({ id }).then(x => x ?? undefined),
);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser>(
Infinity,
(token) => Users.findOneBy({ token }).then(x => x ?? undefined),
);
export const uriPersonCache = new Cache<CacheableUser>(
Infinity,
(uri) => Users.findOneBy({ uri }).then(x => x ?? undefined),
);
subscriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
@ -27,7 +35,6 @@ subscriber.on('message', async (_, data) => {
}
if (Users.isLocalUser(user)) {
localUserByNativeTokenCache.set(user.token, user);
localUserByIdCache.set(user.id, user);
}
break;
}

View file

@ -32,12 +32,8 @@ export async function signout() {
const registration = await navigator.serviceWorker.ready;
const push = await registration.pushManager.getSubscription();
if (push) {
await fetch(`${apiUrl}/sw/unregister`, {
method: 'POST',
body: JSON.stringify({
i: $i.token,
endpoint: push.endpoint,
}),
await api('sw/unregister', {
endpoint: push.endpoint,
});
}
}
@ -79,13 +75,7 @@ export async function removeAccount(id: Account['id']) {
function fetchAccount(token: string): Promise<Account> {
return new Promise((done, fail) => {
// Fetch user
fetch(`${apiUrl}/i`, {
method: 'POST',
body: JSON.stringify({
i: token,
}),
})
.then(res => res.json())
api('i', {}, token)
.then(res => {
if (res.error) {
if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {

View file

@ -5,26 +5,17 @@
:disabled="wait"
@click="onClick"
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked">
<!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
</template>
<template v-if="wait">
<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou">
<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else>
<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
<span v-if="full">{{ user.isLocked ? i18n.ts.followRequest : i18n.ts.follow }}</span><i class="fas fa-plus"></i>
</template>
</button>
</template>

View file

@ -17,6 +17,7 @@
:spellcheck="spellcheck"
:step="step"
:list="id"
:maxlength="max"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@ -58,6 +59,7 @@ const props = defineProps<{
manualSave?: boolean;
small?: boolean;
large?: boolean;
max?: number;
}>();
const emit = defineEmits<{

View file

@ -14,6 +14,7 @@
:pattern="pattern"
:autocomplete="autocomplete ? 'on' : 'off'"
:spellcheck="spellcheck"
:maxlength="max"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@ -54,6 +55,7 @@ const props = withDefaults(defineProps<{
pre?: boolean;
debounce?: boolean;
manualSave?: boolean;
max?: number;
}>(), {
pattern: undefined,
placeholder: '',

View file

@ -31,7 +31,7 @@ const buttonRef = ref<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
const toggleReaction = () => {
const toggleReaction = (): void => {
if (!canToggle.value) return;
const oldReaction = props.note.myReaction;

View file

@ -8,7 +8,6 @@
<div v-if="!totpLogin" class="normal-signin">
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<template #prefix><i class="fas fa-lock"></i></template>
@ -55,7 +54,7 @@ import { showSuspendedDialog } from '@/scripts/show-suspended-dialog';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkInfo from '@/components/ui/info.vue';
import { apiUrl, host as configHost } from '@/config';
import { apiUrl } from '@/config';
import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os';
import { login } from '@/account';
@ -68,7 +67,6 @@ let user = $ref(null);
let username = $ref('');
let password = $ref('');
let token = $ref('');
let host = $ref(toUnicode(configHost));
let totpLogin = $ref(false);
let challengeData = $ref(null);
let queryingKey = $ref(false);

View file

@ -7,7 +7,6 @@
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ i18n.ts.available }}</span>
@ -70,7 +69,6 @@ import MkButton from './ui/button.vue';
import MkCaptcha from './captcha.vue';
import MkInput from './form/input.vue';
import MkSwitch from './form/switch.vue';
import * as config from '@/config';
import * as os from '@/os';
import { login } from '@/account';
import { instance } from '@/instance';
@ -87,8 +85,6 @@ const emit = defineEmits<{
(ev: 'signupEmailPending'): void;
}>();
const host = toUnicode(config.host);
let hcaptcha = $ref();
let recaptcha = $ref();

View file

@ -15,14 +15,14 @@
<div v-else ref="rootEl">
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore(true)">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
</div>
<slot :items="items"></slot>
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore()">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
@ -32,7 +32,7 @@
</template>
<script lang="ts" setup>
import { computed, ComputedRef, isRef, onActivated, onDeactivated, ref, watch } from 'vue';
import { ComputedRef, isRef, onActivated, onDeactivated, watch } from 'vue';
import * as foundkey from 'foundkey-js';
import * as os from '@/os';
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
@ -45,13 +45,13 @@ export type Paging<E extends keyof foundkey.Endpoints = keyof foundkey.Endpoints
params?: foundkey.Endpoints[E]['req'] | ComputedRef<foundkey.Endpoints[E]['req']>;
/**
* 検索APIのようなページング不可なエンドポイントを利用する場合
* (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
* When using non-pageable endpoints, such as the search API.
* (though it is slightly inconsistent to use such an API with this function)
*/
noPaging?: boolean;
/**
* items 配列の中身を逆順にする(新しい方が最後)
* items Array contents in reverse order (newest first, last)
*/
reversed?: boolean;
@ -76,202 +76,179 @@ const emit = defineEmits<{
type Item = { id: string; [another: string]: unknown; };
const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]);
const queue = ref<Item[]>([]);
const offset = ref(0);
const fetching = ref(true);
const moreFetching = ref(false);
const more = ref(false);
const backed = ref(false); //
const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0);
const error = ref(false);
let rootEl: HTMLElement | null = $ref(null);
let items: Item[] = $ref([]);
let queue: Item[] = $ref([]);
let offset: number = $ref(0);
let fetching: boolean = $ref(true);
let moreFetching: boolean = $ref(false);
let more: boolean = $ref(false);
let backed: boolean = $ref(false); //
let isBackTop: boolean = $ref(false);
const empty = $computed(() => items.length === 0);
let error: boolean = $ref(false);
const init = async (): Promise<void> => {
queue.value = [];
fetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
queue = [];
fetching = true;
const params = props.pagination.params
? isRef(props.pagination.params)
? props.pagination.params.value as Record<string, any>
: props.pagination.params
: {};
await os.api(props.pagination.endpoint, {
...params,
limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
}).then(res => {
}).then((res: Item[]) => {
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
res.pop();
items.value = props.pagination.reversed ? [...res].reverse() : res;
more.value = true;
more = true;
} else {
items.value = props.pagination.reversed ? [...res].reverse() : res;
more.value = false;
more = false;
}
offset.value = res.length;
error.value = false;
fetching.value = false;
items = props.pagination.reversed ? [...res].reverse() : res;
offset = res.length;
error = false;
fetching = false;
emit('loaded');
}, () => {
error.value = true;
fetching.value = false;
}).catch(() => {
error = true;
fetching = false;
emit('error');
});
};
const reload = (): void => {
items.value = [];
items = [];
init();
};
const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
moreFetching.value = true;
backed.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
const fetchMore = async (ahead?: boolean): Promise<void> => {
if (!more || fetching || moreFetching || items.length === 0) return;
moreFetching = true;
if (!ahead) {
backed = true;
}
const params = props.pagination.params
? isRef(props.pagination.params)
? props.pagination.params.value as Record<string, any>
: props.pagination.params
: {};
await os.api(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : props.pagination.reversed ? {
sinceId: items.value[0].id,
} : {
untilId: items.value[items.value.length - 1].id,
}),
offset,
} : ahead ? (
props.pagination.reversed ? {
untilId: items[0].id,
} : {
sinceId: items[items.length - 1].id,
}
) : (
props.pagination.reversed ? {
sinceId: items[0].id,
} : {
untilId: items[items.length - 1].id,
}
)),
}).then(res => {
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
more.value = true;
more = true;
} else {
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
more.value = false;
more = false;
}
offset.value += res.length;
moreFetching.value = false;
items = props.pagination.reversed ? [...res].reverse().concat(items) : items.concat(res);
offset += res.length;
moreFetching = false;
}, () => {
moreFetching.value = false;
});
};
const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await os.api(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : props.pagination.reversed ? {
untilId: items.value[0].id,
} : {
sinceId: items.value[items.value.length - 1].id,
}),
}).then(res => {
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
more.value = true;
} else {
items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
more.value = false;
}
offset.value += res.length;
moreFetching.value = false;
}, () => {
moreFetching.value = false;
moreFetching = false;
});
};
const prepend = (item: Item): void => {
if (props.pagination.reversed) {
if (rootEl.value) {
const container = getScrollContainer(rootEl.value);
if (rootEl) {
const container = getScrollContainer(rootEl);
if (container == null) {
// TODO?
} else {
const pos = getScrollPosition(rootEl.value);
const pos = getScrollPosition(rootEl);
const viewHeight = container.clientHeight;
const height = container.scrollHeight;
const isBottom = (pos + viewHeight > height - 32);
// Discard old items if they overflow.
if (isBottom) {
//
if (items.value.length >= props.displayLimit) {
// Vue 3.2
//items.value = items.value.slice(-props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.shift();
}
more.value = true;
while (items.length >= props.displayLimit) {
items.shift();
}
more = true;
}
}
}
items.value.push(item);
items.push(item);
// TODO
} else {
// unshiftOK
if (!rootEl.value) {
items.value.unshift(item);
// Only unshift is required for initial display.
if (!rootEl) {
items.unshift(item);
return;
}
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
const isTop = isBackTop || (document.body.contains(rootEl) && isTopVisible(rootEl));
if (isTop) {
// Prepend the item
items.value.unshift(item);
items.unshift(item);
//
if (items.value.length >= props.displayLimit) {
// Vue 3.2
//this.items = items.value.slice(0, props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.pop();
}
more.value = true;
// Discard old items if they overflow.
while (items.length >= props.displayLimit) {
items.pop();
}
more = true;
} else {
queue.value.push(item);
onScrollTop(rootEl.value, () => {
for (const queueItem of queue.value) {
queue.push(item);
onScrollTop(rootEl, () => {
for (const queueItem of queue) {
prepend(queueItem);
}
queue.value = [];
queue = [];
});
}
}
};
const append = (item: Item): void => {
items.value.push(item);
items.push(item);
};
const removeItem = (finder: (item: Item) => boolean): void => {
const i = items.value.findIndex(finder);
items.value.splice(i, 1);
const i = items.findIndex(finder);
items.splice(i, 1);
};
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
const i = items.value.findIndex(item => item.id === id);
items.value[i] = replacer(items.value[i]);
const i = items.findIndex(item => item.id === id);
items[i] = replacer(items[i]);
};
if (props.pagination.params && isRef(props.pagination.params)) {
watch(props.pagination.params, init, { deep: true });
}
watch(queue, (a, b) => {
if (a.length === 0 && b.length === 0) return;
emit('queue', queue.value.length);
watch($$(queue), (a, b) => {
if (a.length !== 0 || b.length !== 0) emit('queue', queue.length);
}, { deep: true });
init();
onActivated(() => {
isBackTop.value = false;
isBackTop = false;
});
onDeactivated(() => {
isBackTop.value = window.scrollY === 0;
isBackTop = window.scrollY === 0;
});
defineExpose({

View file

@ -2,7 +2,9 @@
<transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="$emit('closed')">
<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
<div v-if="fetched" class="info">
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
</div>
<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
<div class="title">
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@ -105,6 +107,16 @@ onMounted(() => {
background-color: rgba(0, 0, 0, 0.1);
background-size: cover;
background-position: center;
> .followed {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 8px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
font-size: 0.7em;
border-radius: 6px;
}
}
> .avatar {

View file

@ -32,7 +32,10 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache',
headers: authorization ? { authorization } : {},
headers: {
'content-type': 'application/json',
...(authorization ? { authorization } : {}),
},
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -69,7 +72,10 @@ export const apiGet = ((endpoint: string, data: Record<string, any> = {}, token?
method: 'GET',
credentials: 'omit',
cache: 'default',
headers: authorization ? { authorization } : {},
headers: {
'content-type': 'application/json',
...(authorization ? { authorization } : {}),
},
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -97,7 +103,7 @@ export const apiWithDialog = ((
promiseDialog(promise, null, (err) => {
alert({
type: 'error',
text: err.message + '\n' + (err as any).id,
text: (err.message + '\n' + (err?.endpoint ?? '') + (err?.code ?? '')).trim(),
});
});
@ -135,7 +141,7 @@ export function promiseDialog<T extends Promise<any>>(
}
});
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
// NOTE: dynamic import results in strange behaviour (showing is not reactive)
popup(MkWaitingDialog, {
success,
showing,

View file

@ -26,8 +26,27 @@
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
<FormSwitch
:model-value="suspended || isBlocked"
@update:model-value="newValue => {suspended = newValue; toggleSuspend() }"
:disabled="isBlocked"
class="_formBlock"
>
{{ i18n.ts.stopActivityDelivery }}
<template #caption>
{{ i18n.ts.stopActivityDeliveryDescription }}
</template>
</FormSwitch>
<FormSwitch
v-model="isBlocked"
@update:modelValue="toggleBlock"
class="_formBlock"
>
{{ i18n.ts.blockThisInstance }}
<template #caption>
{{ i18n.ts.blockThisInstanceDescription }}
</template>
</FormSwitch>
<MkButton @click="refreshMetadata"><i class="fas fa-refresh"></i> Refresh metadata</MkButton>
</FormSection>
@ -152,7 +171,7 @@ const usersPagination = {
offsetMode: true,
};
async function fetch() {
async function fetch(): Promise<void> {
instance = await os.api('federation/show-instance', {
host: props.host,
});
@ -160,21 +179,21 @@ async function fetch() {
isBlocked = instance.isBlocked;
}
async function toggleBlock(ev) {
async function toggleBlock(): Promise<void> {
if (meta == null) return;
await os.api('admin/update-meta', {
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host),
});
}
async function toggleSuspend(v) {
async function toggleSuspend(): Promise<void> {
await os.api('admin/federation/update-instance', {
host: instance.host,
isSuspended: suspended,
});
}
function refreshMetadata() {
function refreshMetadata(): void {
os.api('admin/federation/refresh-remote-instance-metadata', {
host: instance.host,
});

View file

@ -7,14 +7,14 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { defineAsyncComponent } from 'vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const isDesktop = ref(window.innerWidth >= 1100);
const isDesktop = window.innerWidth >= 1100;
function generateToken() {
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, {

View file

@ -12,7 +12,7 @@
<template #label>{{ i18n.ts._profile.name }}</template>
</FormInput>
<FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
<FormTextarea v-model="profile.description" :max="2048" tall manual-save class="_formBlock">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</FormTextarea>

View file

@ -6,7 +6,6 @@
<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username class="_formBlock">
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-model="password" type="password" data-cy-admin-password class="_formBlock">
<template #label>{{ i18n.ts.password }}</template>
@ -24,7 +23,6 @@
<script lang="ts" setup>
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import { host } from '@/config';
import * as os from '@/os';
import { login } from '@/account';
import { i18n } from '@/i18n';

View file

@ -30,14 +30,6 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
const i18n = await swLang.i18n as I18n<any>;
const { t } = i18n;
switch (data.type) {
/*
case 'driveFileCreated': // TODO (Server Side)
return [t('_notification.fileUploaded'), {
body: body.name,
icon: body.url,
data
}];
*/
case 'notification':
switch (data.body.type) {
case 'follow':

View file

@ -1,6 +1,6 @@
declare var self: ServiceWorkerGlobalScope;
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
import { createNotification } from '@/scripts/create-notification';
import { swLang } from '@/scripts/lang';
import { swNotificationRead } from '@/scripts/notification-read';
import { pushNotificationDataMap } from '@/types';
@ -67,8 +67,6 @@ self.addEventListener('push', ev => {
}
break;
}
return createEmptyNotification();
}));
});

View file

@ -406,46 +406,37 @@ __metadata:
languageName: node
linkType: hard
"@bull-board/api@npm:4.0.0":
version: 4.0.0
resolution: "@bull-board/api@npm:4.0.0"
"@bull-board/api@npm:4.3.1, @bull-board/api@npm:^4.3.1":
version: 4.3.1
resolution: "@bull-board/api@npm:4.3.1"
dependencies:
redis-info: ^3.0.8
checksum: 9d0da26021265c044d1b9bc0cccb728a76385e0c7ec9d7a9b3cd0f6102d971da08c2909562cb9e2635cf485d1c1d9e0d1b2ddec5f8fe0d11b2f48674fd48664a
checksum: 05113b1e888e79f8efecdffdc1043455fa6f8714c55a1e973d8a0a7f60cf574b00487b5b86324523ff91641784a55ff14c469edc8dd985295dcfc27cf55b4c4a
languageName: node
linkType: hard
"@bull-board/api@npm:^4.2.2":
version: 4.2.2
resolution: "@bull-board/api@npm:4.2.2"
"@bull-board/koa@npm:^4.3.1":
version: 4.3.1
resolution: "@bull-board/koa@npm:4.3.1"
dependencies:
redis-info: ^3.0.8
checksum: 547174f63d611a568303ad261d3f41a57f632ea2067e8ab900bad90dbc9790a45af8229765350ef47a2eba4300656e8cc792bdcef94003a531f4eabeb4982876
languageName: node
linkType: hard
"@bull-board/koa@npm:4.0.0":
version: 4.0.0
resolution: "@bull-board/koa@npm:4.0.0"
dependencies:
"@bull-board/api": 4.0.0
"@bull-board/ui": 4.0.0
"@bull-board/api": 4.3.1
"@bull-board/ui": 4.3.1
ejs: ^3.1.7
koa: ^2.13.1
koa-mount: ^4.0.0
koa-router: ^10.0.0
koa-static: ^5.0.0
koa-views: ^7.0.1
checksum: 34b567e46d9d2a1413032f89c8efe4dc30d1844c47588a1ffee2f979698ecefa9a3178c4921746b7a1ac18d999762a96e97e66c5cb031c0074dd39646bb8350f
checksum: 08f198cdaaa28fe8e254288a0d4c13e9cd481a97e40e5e9152fb9094cbac54459e86901da5d90c46fe2dccf310f78a50ef8763bf5980b98d33180299c64fbc3f
languageName: node
linkType: hard
"@bull-board/ui@npm:4.0.0":
version: 4.0.0
resolution: "@bull-board/ui@npm:4.0.0"
"@bull-board/ui@npm:4.3.1":
version: 4.3.1
resolution: "@bull-board/ui@npm:4.3.1"
dependencies:
"@bull-board/api": 4.0.0
checksum: 34d90de90137587e0343a2cfe236df4f7c667a6f08e0c1ea5516090c481b5acbce10ebb652b705f9eb0272b3f6a98033d160e2906b0a120bc76b3c89cf303340
"@bull-board/api": 4.3.1
checksum: 7bc4787ba8f9e3dda5cb580b4374872bc7b0870a08a504cfc2f380a39dda164ae71518e7b1921e53ff8abc4224c0861504b26b63510b6c9c9d23d647bdab54b2
languageName: node
linkType: hard
@ -3618,8 +3609,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "backend@workspace:packages/backend"
dependencies:
"@bull-board/api": ^4.2.2
"@bull-board/koa": 4.0.0
"@bull-board/api": ^4.3.1
"@bull-board/koa": ^4.3.1
"@discordapp/twemoji": 14.0.2
"@elastic/elasticsearch": 7.11.0
"@koa/cors": 3.1.0
@ -3752,7 +3743,7 @@ __metadata:
rss-parser: 3.12.0
sanitize-html: 2.7.0
semver: 7.3.7
sharp: 0.30.7
sharp: 0.31.2
speakeasy: 2.0.0
strict-event-emitter-types: 2.0.0
stringz: 2.1.0
@ -15198,6 +15189,17 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.8":
version: 7.3.8
resolution: "semver@npm:7.3.8"
dependencies:
lru-cache: ^6.0.0
bin:
semver: bin/semver.js
checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1
languageName: node
linkType: hard
"serialize-javascript@npm:6.0.0":
version: 6.0.0
resolution: "serialize-javascript@npm:6.0.0"
@ -15259,20 +15261,20 @@ __metadata:
languageName: node
linkType: hard
"sharp@npm:0.30.7":
version: 0.30.7
resolution: "sharp@npm:0.30.7"
"sharp@npm:0.31.2":
version: 0.31.2
resolution: "sharp@npm:0.31.2"
dependencies:
color: ^4.2.3
detect-libc: ^2.0.1
node-addon-api: ^5.0.0
node-gyp: latest
prebuild-install: ^7.1.1
semver: ^7.3.7
semver: ^7.3.8
simple-get: ^4.0.1
tar-fs: ^2.1.1
tunnel-agent: ^0.6.0
checksum: bbc63ca3c7ea8a5bff32cd77022cfea30e25a03f5bd031e935924bf6cf0e11e3388e8b0e22b3137bf8816aa73407f1e4fbeb190f3a35605c27ffca9f32b91601
checksum: 076717b7a073ea47bb522ff2931b74b6608daeb6f7ae334e4848d47fdf4d23bcb18cd49044fd5fb27ef27a1a4aa87d141894d67d1c4bb15a6e2e63cf4dbe329e
languageName: node
linkType: hard