forked from FoundKeyGang/FoundKey
Compare commits
33 commits
fe99f4de6f
...
a4c9ab8dd1
Author | SHA1 | Date | |
---|---|---|---|
a4c9ab8dd1 | |||
04d4dd323f | |||
e814fdc7d1 | |||
f2f547172e | |||
f17485d8a2 | |||
70eec26b74 | |||
a74c1d9126 | |||
811d5cd0d7 | |||
d762143b89 | |||
21c1e5c06c | |||
91a4f38871 | |||
756ecbb1f7 | |||
b431471fd1 | |||
48023a0814 | |||
8721e8844a | |||
4b98677141 | |||
177f34b02e | |||
79c70c1017 | |||
cb0b14ba2d | |||
7ea6deb19b | |||
6d0cfe42f2 | |||
7cd11e7afd | |||
0b8fa2665c | |||
421b42d07d | |||
9a503273fb | |||
20a6140e9a | |||
c29e24c103 | |||
2c411d59f4 | |||
683b0cfa82 | |||
c65559872b | |||
8920eeb86a | |||
4f9504d135 | |||
9022ab9f2a |
41 changed files with 285 additions and 231 deletions
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -11,37 +11,84 @@ Unreleased changes should not be listed in this file.
|
||||||
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
||||||
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||||
|
|
||||||
## Unreleased
|
## 13.0.0-preview2 - 2022-10-16
|
||||||
|
### Security
|
||||||
|
- server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
|
||||||
|
- server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Client: Show instance info in ticker
|
- allow to mute only renotes of a user
|
||||||
- Client: Readded group pages
|
- allow to export only selected custom emoji
|
||||||
- Client: add re-collapsing to quoted notes
|
- client: improve emoji picker search
|
||||||
|
- client: Extend Emoji list
|
||||||
|
- client: show alt text in image viewer
|
||||||
|
- client: Show instance info in ticker
|
||||||
|
- client: Readded group pages
|
||||||
|
- client: add re-collapsing to quoted notes
|
||||||
|
- server: allow files storage path to be set explicitly
|
||||||
|
- server: refactor expiring data and expire signins after 60 days
|
||||||
|
- server: send delete activity to all known instances
|
||||||
|
- server: add automatic dead instance detection
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Client: Use consistent date formatting based on language setting
|
- foundkey-js: Sync possible endpoints from backend
|
||||||
- Client: Add threshold to reduce occurances of "future" timestamps
|
- foundkey-js: update LiteInstanceMetadata fields
|
||||||
- Pages have been considerably simplified, several of the very complex features have been removed.
|
- meta: use parallel and incremental builds
|
||||||
|
- meta: update WORKDIR to foundkey
|
||||||
|
- meta: update dependencies
|
||||||
|
- client: consolidate about & notifications pages
|
||||||
|
- client: include renote in visibility computation
|
||||||
|
- client: make emoji amount slider more intuitive
|
||||||
|
- client: sort emojis by query similarity in fuzzy picker
|
||||||
|
- client: discard drafts that are just the default state
|
||||||
|
- client: Use consistent date formatting based on language setting
|
||||||
|
- client: Add threshold to reduce occurances of "future" timestamps
|
||||||
|
- server: mute notifications in muted threads
|
||||||
|
- server: allow for source lang to be overridden in note/translate
|
||||||
|
- server: allow redis family to be specified as a string
|
||||||
|
- server: increase image description limit to 2048 characters
|
||||||
|
- server: Pages have been considerably simplified, several of the very complex features have been removed.
|
||||||
Pages are now MFM only.
|
Pages are now MFM only.
|
||||||
**For admins:** There is a migration in place to convert page contents to text, but not everything can be migrated.
|
**For admins:** There is a migration in place to convert page contents to text, but not everything can be migrated.
|
||||||
You might want to check if you have any more complex pages on your instance and ask users to migrate them by hand.
|
You might want to check if you have any more complex pages on your instance and ask users to migrate them by hand.
|
||||||
Or generally advise all users to simplify their pages to only text.
|
Or generally advise all users to simplify their pages to only text.
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Okteto config and Helm chart
|
|
||||||
- Client: acrylic styling
|
|
||||||
- Client: Twitter embeds, the standard URL preview is used instead.
|
|
||||||
- Promotion entities and endpoints
|
|
||||||
- Server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
|
|
||||||
Foundkey will now work as if it was set to `true`.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Client: Notifications for ended polls can now be turned off
|
- client: alt text dialog properly handles non-images
|
||||||
- Client: Emoji picker should load faster now
|
- client: Fix style scoping in MkMention
|
||||||
- Server: Blocking remote accounts
|
- client: default instance ticker name to instance's domain name
|
||||||
|
- client: improve error message for empty gallery posts
|
||||||
|
- client: fix default-selected reply scopes
|
||||||
|
- client: Make MFM cheatsheet interactive again
|
||||||
|
- client: Fix reports not showing in control panel
|
||||||
|
- client: make hard coded strings in emoji admin panel internationalized
|
||||||
|
- client: Notifications for ended polls can now be turned off
|
||||||
|
- client: improve emoji picker performance
|
||||||
|
- server: Blocking remote accounts
|
||||||
|
- server: fix table name used in toHtml
|
||||||
|
- server: Fix appendChildren TypeError
|
||||||
|
- server: ensure only own notifications can be marked as read
|
||||||
|
- server: render HTML mentions correctly
|
||||||
|
- server: increase requestId max size for GNU Social
|
||||||
|
- server: fix HTTP GET parameters in OpenAPI docs
|
||||||
|
- server: proper error messages for creating accounts
|
||||||
|
- server: Fix thread muting queries
|
||||||
|
- docker: add built foundkey-js files to container
|
||||||
|
- service worker: Remove fetch handler from service worker
|
||||||
|
|
||||||
### Security
|
### Removed
|
||||||
- Server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
|
- remove misskey-assets submodule
|
||||||
- Server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
|
- server: remove room data from user
|
||||||
|
- client: remove ai mode
|
||||||
|
- client: remove "Disable AiScript on Pages" setting
|
||||||
|
- client: acrylic styling
|
||||||
|
- client: Twitter embeds, the standard URL preview is used instead.
|
||||||
|
- foundkey-js: remove room api endpoints
|
||||||
|
- server: remove unusable setting to send error reports
|
||||||
|
- server: ignore detail parameter on meta endpoint
|
||||||
|
- server: Promotion entities and endpoints
|
||||||
|
- server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
|
||||||
|
Foundkey will now work as if it was set to `true`.
|
||||||
|
|
||||||
## 13.0.0-preview1 - 2022-08-05
|
## 13.0.0-preview1 - 2022-08-05
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Reporting Security Issues
|
# Reporting Security Issues
|
||||||
|
|
||||||
If you discover a security issue in Misskey, please report it by sending an
|
If you discover a security issue in Foundkey, please report it by sending an
|
||||||
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
|
email to [johann@qwertqwefsday.eu](mailto:johann@qwertqwefsday.eu).
|
||||||
|
|
||||||
This will allow us to assess the risk, and make a fix available before we add a
|
This will allow us to assess the risk, and make a fix available before we add a
|
||||||
bug report to the GitHub repository.
|
bug report to the repository.
|
||||||
|
|
||||||
Thanks for helping make Misskey safe for everyone.
|
Thanks for helping make Foundkey safe for everyone.
|
||||||
|
|
|
@ -65,6 +65,8 @@ directNotes: "Direct notes"
|
||||||
importAndExport: "Import / Export"
|
importAndExport: "Import / Export"
|
||||||
import: "Import"
|
import: "Import"
|
||||||
export: "Export"
|
export: "Export"
|
||||||
|
exportAll: "Export all"
|
||||||
|
exportSelected: "Export selected"
|
||||||
files: "Files"
|
files: "Files"
|
||||||
download: "Download"
|
download: "Download"
|
||||||
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes\
|
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes\
|
||||||
|
@ -920,6 +922,13 @@ thereIsUnresolvedAbuseReportWarning: "There are unsolved reports."
|
||||||
recommended: "Recommended"
|
recommended: "Recommended"
|
||||||
check: "Check"
|
check: "Check"
|
||||||
unlimited: "Unlimited"
|
unlimited: "Unlimited"
|
||||||
|
selectMode: "Select multiple"
|
||||||
|
selectAll: "Select all"
|
||||||
|
setCategory: "Set category"
|
||||||
|
setTag: "Set tag"
|
||||||
|
addTag: "Add tag"
|
||||||
|
removeTag: "Remove tag"
|
||||||
|
externalCssSnippets: "Some CSS snippets for your inspiration (not managed by FoundKey)"
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "This email address is already being used"
|
used: "This email address is already being used"
|
||||||
format: "The format of this email address is invalid"
|
format: "The format of this email address is invalid"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "foundkey",
|
"name": "foundkey",
|
||||||
"version": "13.0.0-preview.1",
|
"version": "13.0.0-preview2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "13.0.0-preview1",
|
"version": "13.0.0-preview2",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
@ -84,7 +84,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
||||||
return groupBy((a, b) => f(a) === f(b), xs);
|
return groupBy((a, b) => f(a) === f(b), xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
export function groupByX<T>(collections: T[], keySelector: (x: T) => string): Record<string, T[]> {
|
||||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
||||||
const key = keySelector(item);
|
const key = keySelector(item);
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function gcd(a: number, b: number): number {
|
|
||||||
return b === 0 ? a : gcd(b, a % b);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
export interface IMaybe<T> {
|
|
||||||
isJust(): this is IJust<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IJust<T> extends IMaybe<T> {
|
|
||||||
get(): T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function just<T>(value: T): IJust<T> {
|
|
||||||
return {
|
|
||||||
isJust: () => true,
|
|
||||||
get: () => value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nothing<T>(): IMaybe<T> {
|
|
||||||
return {
|
|
||||||
isJust: () => false,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
export function concat(xs: string[]): string {
|
|
||||||
return xs.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function capitalize(s: string): string {
|
|
||||||
return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toUpperCase(s: string): string {
|
|
||||||
return s.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toLowerCase(s: string): string {
|
|
||||||
return s.toLowerCase();
|
|
||||||
}
|
|
|
@ -131,9 +131,10 @@ export function createDeleteDriveFilesJob(user: ThinUser) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createExportCustomEmojisJob(user: ThinUser) {
|
export function createExportCustomEmojisJob(user: ThinUser, ids: string[] | undefined) {
|
||||||
return dbQueue.add('exportCustomEmojis', {
|
return dbQueue.add('exportCustomEmojis', {
|
||||||
user,
|
user,
|
||||||
|
ids,
|
||||||
}, {
|
}, {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import archiver from 'archiver';
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
import { IsNull } from 'typeorm';
|
import { In, IsNull } from 'typeorm';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
||||||
import { downloadUrl } from '@/misc/download-url.js';
|
import { downloadUrl } from '@/misc/download-url.js';
|
||||||
|
@ -50,6 +50,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
||||||
const customEmojis = await Emojis.find({
|
const customEmojis = await Emojis.find({
|
||||||
where: {
|
where: {
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
|
...(job.data.ids ? { id: In(job.data.ids) } : {}),
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
id: 'ASC',
|
id: 'ASC',
|
||||||
|
|
|
@ -12,34 +12,32 @@ import { Cache } from '@/misc/cache.js';
|
||||||
import { Instance } from '@/models/entities/instance.js';
|
import { Instance } from '@/models/entities/instance.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
import { DeliverJobData } from '@/queue/types.js';
|
import { DeliverJobData } from '@/queue/types.js';
|
||||||
|
import { LessThan } from 'typeorm';
|
||||||
|
import { DAY } from '@/const.js';
|
||||||
|
|
||||||
const logger = new Logger('deliver');
|
const logger = new Logger('deliver');
|
||||||
|
|
||||||
let latest: string | null = null;
|
let latest: string | null = null;
|
||||||
|
|
||||||
const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
const deadThreshold = 30 * DAY;
|
||||||
|
|
||||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
|
const puny = toPuny(host);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
if (meta.blockedHosts.includes(toPuny(host))) {
|
if (meta.blockedHosts.includes(puny)) {
|
||||||
return 'skip (blocked)';
|
return 'skip (blocked)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSuspendedなら中断
|
const deadTime = new Date(Date.now() - deadThreshold);
|
||||||
let suspendedHosts = suspendedHostsCache.get(null);
|
const isSuspendedOrDead = await Instances.countBy([
|
||||||
if (suspendedHosts == null) {
|
{ host: puny, isSuspended: true },
|
||||||
suspendedHosts = await Instances.find({
|
{ host: puny, lastCommunicatedAt: LessThan(deadTime) },
|
||||||
where: {
|
]);
|
||||||
isSuspended: true,
|
if (isSuspendedOrDead) {
|
||||||
},
|
return 'skip (suspended or dead)';
|
||||||
});
|
|
||||||
suspendedHostsCache.set(null, suspendedHosts);
|
|
||||||
}
|
|
||||||
if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
|
|
||||||
return 'skip (suspended)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -8,6 +8,10 @@ interface IRecipe {
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IEveryoneRecipe extends IRecipe {
|
||||||
|
type: 'Everyone';
|
||||||
|
}
|
||||||
|
|
||||||
interface IFollowersRecipe extends IRecipe {
|
interface IFollowersRecipe extends IRecipe {
|
||||||
type: 'Followers';
|
type: 'Followers';
|
||||||
}
|
}
|
||||||
|
@ -17,6 +21,9 @@ interface IDirectRecipe extends IRecipe {
|
||||||
to: IRemoteUser;
|
to: IRemoteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEveryone = (recipe: any): recipe is IEveryoneRecipe =>
|
||||||
|
recipe.type === 'Everyone';
|
||||||
|
|
||||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||||
recipe.type === 'Followers';
|
recipe.type === 'Followers';
|
||||||
|
|
||||||
|
@ -63,6 +70,13 @@ export default class DeliverManager {
|
||||||
this.addRecipe(recipe);
|
this.addRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add recipe to send this activity to all known sharedInboxes
|
||||||
|
*/
|
||||||
|
public addEveryone() {
|
||||||
|
this.addRecipe({ type: 'Everyone' } as IEveryoneRecipe);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add recipe
|
* Add recipe
|
||||||
* @param recipe Recipe
|
* @param recipe Recipe
|
||||||
|
@ -82,9 +96,26 @@ export default class DeliverManager {
|
||||||
/*
|
/*
|
||||||
build inbox list
|
build inbox list
|
||||||
|
|
||||||
Process follower recipes first to avoid duplication when processing
|
Processing order matters to avoid duplication.
|
||||||
direct recipes later.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (this.recipes.some(r => isEveryone(r))) {
|
||||||
|
// deliver to all of known network
|
||||||
|
const sharedInboxes = await Users.createQueryBuilder('users')
|
||||||
|
.select('users.sharedInbox', 'sharedInbox')
|
||||||
|
// so we don't have to make our inboxes Set work as hard
|
||||||
|
.distinct(true)
|
||||||
|
// can't deliver to unknown shared inbox
|
||||||
|
.where('users.sharedInbox IS NOT NULL')
|
||||||
|
// don't deliver to ourselves
|
||||||
|
.andWhere('users.host IS NOT NULL')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
for (const inbox of sharedInboxes) {
|
||||||
|
inboxes.add(inbox.sharedInbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.recipes.some(r => isFollowers(r))) {
|
if (this.recipes.some(r => isFollowers(r))) {
|
||||||
// followers deliver
|
// followers deliver
|
||||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||||
|
|
|
@ -13,11 +13,19 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {},
|
properties: {
|
||||||
|
ids: {
|
||||||
|
description: 'Specific emoji IDs to be exported. Non-local emoji will be ignored. If not provided, all local emoji will be exported.',
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
minItems: 1,
|
||||||
|
uniqueItems: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
createExportCustomEmojisJob(user);
|
createExportCustomEmojisJob(user, ps.ids);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,9 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -253,7 +256,12 @@ export const meta = {
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
detail: { type: 'boolean', default: true },
|
detail: {
|
||||||
|
deprecated: true,
|
||||||
|
description: 'This parameter is ignored. You will always get all details (as if it was `true`).',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -276,7 +284,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: any = {
|
return {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
|
||||||
|
@ -317,21 +325,16 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable: instance.deeplAuthKey != null,
|
||||||
|
|
||||||
...(ps.detail ? {
|
pinnedPages: instance.pinnedPages,
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
pinnedClipId: instance.pinnedClipId,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
requireSetup: (await Users.countBy({
|
||||||
requireSetup: (await Users.countBy({
|
host: IsNull(),
|
||||||
host: IsNull(),
|
})) === 0,
|
||||||
})) === 0,
|
|
||||||
} : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ps.detail) {
|
proxyAccountName: instance.proxyAccountId ? (await Users.pack(instance.proxyAccountId).catch(() => null))?.username : null,
|
||||||
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
|
|
||||||
|
|
||||||
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
|
features: {
|
||||||
response.features = {
|
|
||||||
registration: !instance.disableRegistration,
|
registration: !instance.disableRegistration,
|
||||||
localTimeLine: !instance.disableLocalTimeline,
|
localTimeLine: !instance.disableLocalTimeline,
|
||||||
globalTimeLine: !instance.disableGlobalTimeline,
|
globalTimeLine: !instance.disableGlobalTimeline,
|
||||||
|
@ -345,8 +348,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
discord: instance.enableDiscordIntegration,
|
discord: instance.enableDiscordIntegration,
|
||||||
serviceWorker: instance.enableServiceWorker,
|
serviceWorker: instance.enableServiceWorker,
|
||||||
miauth: true,
|
miauth: true,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { beforeShutdown } from '@/misc/before-shutdown.js';
|
import { beforeShutdown } from '@/misc/before-shutdown.js';
|
||||||
|
|
||||||
|
import { MINUTE } from '@/const.js';
|
||||||
import FederationChart from './charts/federation.js';
|
import FederationChart from './charts/federation.js';
|
||||||
import NotesChart from './charts/notes.js';
|
import NotesChart from './charts/notes.js';
|
||||||
import UsersChart from './charts/users.js';
|
import UsersChart from './charts/users.js';
|
||||||
|
@ -41,11 +42,11 @@ const charts = [
|
||||||
apRequestChart,
|
apRequestChart,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 20分おきにメモリ情報をDBに書き込み
|
// Write memory information to DB every 20 minutes
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
for (const chart of charts) {
|
for (const chart of charts) {
|
||||||
chart.save();
|
chart.save();
|
||||||
}
|
}
|
||||||
}, 1000 * 60 * 20);
|
}, 20 * MINUTE);
|
||||||
|
|
||||||
beforeShutdown(() => Promise.all(charts.map(chart => chart.save())));
|
beforeShutdown(() => Promise.all(charts.map(chart => chart.save())));
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { IRemoteUser, User } from '@/models/entities/user.js';
|
||||||
import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js';
|
import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
|
import { DriveFolder } from '@/models/entities/drive-folder.js';
|
||||||
import { deleteFile } from './delete-file.js';
|
import { deleteFile } from './delete-file.js';
|
||||||
import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
|
import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
|
||||||
import { driveLogger } from './logger.js';
|
import { driveLogger } from './logger.js';
|
||||||
|
@ -153,7 +154,10 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
|
||||||
* @param type Content-Type for original
|
* @param type Content-Type for original
|
||||||
* @param generateWeb Generate webpublic or not
|
* @param generateWeb Generate webpublic or not
|
||||||
*/
|
*/
|
||||||
export async function generateAlts(path: string, type: string, generateWeb: boolean) {
|
export async function generateAlts(path: string, type: string, generateWeb: boolean): Promise<{
|
||||||
|
webpublic: IImage | null;
|
||||||
|
thumbnail: IImage | null;
|
||||||
|
}> {
|
||||||
if (type.startsWith('video/')) {
|
if (type.startsWith('video/')) {
|
||||||
try {
|
try {
|
||||||
const thumbnail = await GenerateVideoThumbnail(path);
|
const thumbnail = await GenerateVideoThumbnail(path);
|
||||||
|
@ -256,7 +260,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
|
||||||
/**
|
/**
|
||||||
* Upload to ObjectStorage
|
* Upload to ObjectStorage
|
||||||
*/
|
*/
|
||||||
async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string, filename?: string) {
|
async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string, filename?: string): Promise<void> {
|
||||||
const type = (_type === 'image/apng')
|
const type = (_type === 'image/apng')
|
||||||
? 'image/png'
|
? 'image/png'
|
||||||
: (FILE_TYPE_BROWSERSAFE.includes(_type))
|
: (FILE_TYPE_BROWSERSAFE.includes(_type))
|
||||||
|
@ -286,7 +290,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string
|
||||||
if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteOldFile(user: IRemoteUser) {
|
async function deleteOldFile(user: IRemoteUser): Promise<void> {
|
||||||
const q = DriveFiles.createQueryBuilder('file')
|
const q = DriveFiles.createQueryBuilder('file')
|
||||||
.where('file.userId = :userId', { userId: user.id })
|
.where('file.userId = :userId', { userId: user.id })
|
||||||
.andWhere('file.isLink = FALSE');
|
.andWhere('file.isLink = FALSE');
|
||||||
|
@ -387,7 +391,7 @@ export async function addFile({
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const fetchFolder = async () => {
|
const fetchFolder = async (): Promise<DriveFolder | null> => {
|
||||||
if (!folderId) {
|
if (!folderId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -425,7 +429,7 @@ export async function addFile({
|
||||||
file.createdAt = new Date();
|
file.createdAt = new Date();
|
||||||
file.userId = user ? user.id : null;
|
file.userId = user ? user.id : null;
|
||||||
file.userHost = user ? user.host : null;
|
file.userHost = user ? user.host : null;
|
||||||
file.folderId = folder?.id;
|
file.folderId = folder?.id ?? null;
|
||||||
file.comment = comment;
|
file.comment = comment;
|
||||||
file.properties = properties;
|
file.properties = properties;
|
||||||
file.blurhash = info.blurhash || null;
|
file.blurhash = info.blurhash || null;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { InternalStorage } from './internal-storage.js';
|
import { InternalStorage } from './internal-storage.js';
|
||||||
import { getS3 } from './s3.js';
|
import { getS3 } from './s3.js';
|
||||||
|
|
||||||
export async function deleteFile(file: DriveFile, isExpired = false) {
|
export async function deleteFile(file: DriveFile, isExpired = false): Promise<void> {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
InternalStorage.del(file.accessKey!);
|
InternalStorage.del(file.accessKey!);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export async function deleteFile(file: DriveFile, isExpired = false) {
|
||||||
postProcess(file, isExpired);
|
postProcess(file, isExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFileSync(file: DriveFile, isExpired = false) {
|
export async function deleteFileSync(file: DriveFile, isExpired = false): Promise<void> {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
InternalStorage.del(file.accessKey!);
|
InternalStorage.del(file.accessKey!);
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export async function deleteFileSync(file: DriveFile, isExpired = false) {
|
||||||
postProcess(file, isExpired);
|
postProcess(file, isExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postProcess(file: DriveFile, isExpired = false) {
|
async function postProcess(file: DriveFile, isExpired = false): Promise<void> {
|
||||||
// リモートファイル期限切れ削除後は直リンクにする
|
// リモートファイル期限切れ削除後は直リンクにする
|
||||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
DriveFiles.update(file.id, {
|
DriveFiles.update(file.id, {
|
||||||
|
@ -89,7 +89,7 @@ async function postProcess(file: DriveFile, isExpired = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteObjectStorageFile(key: string) {
|
export async function deleteObjectStorageFile(key: string): Promise<void> {
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
|
|
||||||
const s3 = getS3(meta);
|
const s3 = getS3(meta);
|
||||||
|
|
|
@ -11,7 +11,7 @@ export type IImage = {
|
||||||
* with resize, remove metadata, resolve orientation, stop animation
|
* with resize, remove metadata, resolve orientation, stop animation
|
||||||
*/
|
*/
|
||||||
export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
|
export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
|
||||||
return convertSharpToJpeg(await sharp(path), width, height);
|
return convertSharpToJpeg(sharp(path), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
|
export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
|
||||||
|
@ -39,7 +39,7 @@ export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, heig
|
||||||
* with resize, remove metadata, resolve orientation, stop animation
|
* with resize, remove metadata, resolve orientation, stop animation
|
||||||
*/
|
*/
|
||||||
export async function convertToWebp(path: string, width: number, height: number, quality = 85): Promise<IImage> {
|
export async function convertToWebp(path: string, width: number, height: number, quality = 85): Promise<IImage> {
|
||||||
return convertSharpToWebp(await sharp(path), width, height, quality);
|
return convertSharpToWebp(sharp(path), width, height, quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise<IImage> {
|
export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise<IImage> {
|
||||||
|
@ -66,7 +66,7 @@ export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, heig
|
||||||
* with resize, remove metadata, resolve orientation, stop animation
|
* with resize, remove metadata, resolve orientation, stop animation
|
||||||
*/
|
*/
|
||||||
export async function convertToPng(path: string, width: number, height: number): Promise<IImage> {
|
export async function convertToPng(path: string, width: number, height: number): Promise<IImage> {
|
||||||
return convertSharpToPng(await sharp(path), width, height);
|
return convertSharpToPng(sharp(path), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
|
export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
|
||||||
|
|
|
@ -10,25 +10,25 @@ const _dirname = dirname(_filename);
|
||||||
export class InternalStorage {
|
export class InternalStorage {
|
||||||
private static readonly path = config.internalStoragePath || Path.resolve(_dirname, '../../../../../files');
|
private static readonly path = config.internalStoragePath || Path.resolve(_dirname, '../../../../../files');
|
||||||
|
|
||||||
public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);
|
public static resolvePath = (key: string): string => Path.resolve(InternalStorage.path, key);
|
||||||
|
|
||||||
public static read(key: string) {
|
public static read(key: string): fs.ReadStream {
|
||||||
return fs.createReadStream(InternalStorage.resolvePath(key));
|
return fs.createReadStream(InternalStorage.resolvePath(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static saveFromPath(key: string, srcPath: string) {
|
public static saveFromPath(key: string, srcPath: string): string {
|
||||||
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
||||||
fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
|
fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
|
||||||
return `${config.url}/files/${key}`;
|
return `${config.url}/files/${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static saveFromBuffer(key: string, data: Buffer) {
|
public static saveFromBuffer(key: string, data: Buffer): string {
|
||||||
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
fs.mkdirSync(InternalStorage.path, { recursive: true });
|
||||||
fs.writeFileSync(InternalStorage.resolvePath(key), data);
|
fs.writeFileSync(InternalStorage.resolvePath(key), data);
|
||||||
return `${config.url}/files/${key}`;
|
return `${config.url}/files/${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static del(key: string) {
|
public static del(key: string): void {
|
||||||
fs.unlink(InternalStorage.resolvePath(key), () => {});
|
fs.unlink(InternalStorage.resolvePath(key), () => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import S3 from 'aws-sdk/clients/s3.js';
|
||||||
import { Meta } from '@/models/entities/meta.js';
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { getAgentByUrl } from '@/misc/fetch.js';
|
import { getAgentByUrl } from '@/misc/fetch.js';
|
||||||
|
|
||||||
export function getS3(meta: Meta) {
|
export function getS3(meta: Meta): S3 {
|
||||||
const u = meta.objectStorageEndpoint != null
|
const u = meta.objectStorageEndpoint != null
|
||||||
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
|
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
|
||||||
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
|
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
|
||||||
|
|
|
@ -58,7 +58,7 @@ export async function uploadFromUrl({
|
||||||
sensitive,
|
sensitive,
|
||||||
});
|
});
|
||||||
logger.succ(`Got: ${driveFile.id}`);
|
logger.succ(`Got: ${driveFile.id}`);
|
||||||
return driveFile!;
|
return driveFile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to create drive file: ${e}`, {
|
logger.error(`Failed to create drive file: ${e}`, {
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -12,11 +12,11 @@ import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js';
|
||||||
import { deliverToRelays } from '../relay.js';
|
import { deliverToRelays } from '../relay.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定した投稿をピン留めします
|
* Pin a given post to a user profile.
|
||||||
* @param user
|
* @param user the user to pin the note to
|
||||||
* @param noteId
|
* @param noteId ID of note to pin
|
||||||
*/
|
*/
|
||||||
export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
|
export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']): Promise<void> {
|
||||||
// Fetch pinee
|
// Fetch pinee
|
||||||
const note = await Notes.findOneBy({
|
const note = await Notes.findOneBy({
|
||||||
id: noteId,
|
id: noteId,
|
||||||
|
@ -51,11 +51,11 @@ export async function addPinned(user: { id: User['id']; host: User['host']; }, n
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定した投稿のピン留めを解除します
|
* Unpin a given post from a user profile.
|
||||||
* @param user
|
* @param user the user to unpin a note from
|
||||||
* @param noteId
|
* @param noteId ID of note to unpin
|
||||||
*/
|
*/
|
||||||
export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
|
export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']): Promise<void> {
|
||||||
// Fetch unpinee
|
// Fetch unpinee
|
||||||
const note = await Notes.findOneBy({
|
const note = await Notes.findOneBy({
|
||||||
id: noteId,
|
id: noteId,
|
||||||
|
@ -77,7 +77,13 @@ export async function removePinned(user: { id: User['id']; host: User['host']; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) {
|
/**
|
||||||
|
* Notify followers and relays when a note is pinned/unpinned.
|
||||||
|
* @param userId ID of user
|
||||||
|
* @param noteId ID of note
|
||||||
|
* @param isAddition whether the note was pinned or unpinned
|
||||||
|
*/
|
||||||
|
export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean): Promise<void> {
|
||||||
const user = await Users.findOneBy({ id: userId });
|
const user = await Users.findOneBy({ id: userId });
|
||||||
if (user == null) throw new Error('user not found');
|
if (user == null) throw new Error('user not found');
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,15 @@ import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
||||||
import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js';
|
import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js';
|
||||||
import { deliverToRelays } from '../relay.js';
|
import { deliverToRelays } from '../relay.js';
|
||||||
|
|
||||||
export async function publishToFollowers(userId: User['id']) {
|
/**
|
||||||
|
* Send an Update activity to a user's followers.
|
||||||
|
* @param userId ID of user
|
||||||
|
*/
|
||||||
|
export async function publishToFollowers(userId: User['id']): Promise<void> {
|
||||||
const user = await Users.findOneBy({ id: userId });
|
const user = await Users.findOneBy({ id: userId });
|
||||||
if (user == null) throw new Error('user not found');
|
if (user == null) throw new Error('user not found');
|
||||||
|
|
||||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
|
// Deliver Update if the follower is a remote user and the poster is a local user
|
||||||
if (Users.isLocalUser(user)) {
|
if (Users.isLocalUser(user)) {
|
||||||
const content = renderActivity(renderUpdate(await renderPerson(user), user));
|
const content = renderActivity(renderUpdate(await renderPerson(user), user));
|
||||||
deliverToFollowers(user, content);
|
deliverToFollowers(user, content);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
import { Notes, Users, Instances } from '@/models/index.js';
|
import { Notes, Users, Instances } from '@/models/index.js';
|
||||||
import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js';
|
import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js';
|
||||||
import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js';
|
import DeliverManager from '@/remote/activitypub/deliver-manager.js';
|
||||||
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
||||||
import { isPureRenote } from '@/misc/renote.js';
|
import { isPureRenote } from '@/misc/renote.js';
|
||||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||||
|
@ -86,7 +86,7 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
|
||||||
async function findCascadingNotes(note: Note): Promise<Note[]> {
|
async function findCascadingNotes(note: Note): Promise<Note[]> {
|
||||||
const cascadingNotes: Note[] = [];
|
const cascadingNotes: Note[] = [];
|
||||||
|
|
||||||
const recursive = async (noteId: string) => {
|
const recursive = async (noteId: string): Promise<void> => {
|
||||||
const query = Notes.createQueryBuilder('note')
|
const query = Notes.createQueryBuilder('note')
|
||||||
.where('note.replyId = :noteId', { noteId })
|
.where('note.replyId = :noteId', { noteId })
|
||||||
.orWhere(new Brackets(q => {
|
.orWhere(new Brackets(q => {
|
||||||
|
@ -109,7 +109,7 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
|
||||||
const where = [] as any[];
|
const where = [] as any[];
|
||||||
|
|
||||||
// mention / reply / dm
|
// mention / reply / dm
|
||||||
if (note.mentions > 0) {
|
if (note.mentions.length > 0) {
|
||||||
where.push({
|
where.push({
|
||||||
id: In(note.mentions),
|
id: In(note.mentions),
|
||||||
// only remote users, local users are on the server and do not need to be notified
|
// only remote users, local users are on the server and do not need to be notified
|
||||||
|
@ -131,11 +131,23 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
|
||||||
}) as IRemoteUser[];
|
}) as IRemoteUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) {
|
async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any): Promise<void> {
|
||||||
deliverToFollowers(user, content);
|
const manager = new DeliverManager(user, content);
|
||||||
deliverToRelays(user, content);
|
|
||||||
const remoteUsers = await getMentionedRemoteUsers(note);
|
const remoteUsers = await getMentionedRemoteUsers(note);
|
||||||
for (const remoteUser of remoteUsers) {
|
for (const remoteUser of remoteUsers) {
|
||||||
deliverToUser(user, content, remoteUser);
|
manager.addDirectRecipe(remoteUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||||
|
manager.addFollowersRecipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['public', 'home'].includes(note.visibility)) {
|
||||||
|
manager.addEveryone();
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.execute();
|
||||||
|
|
||||||
|
deliverToRelays(user, content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import { just, nothing } from '../../src/prelude/maybe.js';
|
|
||||||
|
|
||||||
describe('just', () => {
|
|
||||||
it('has a value', () => {
|
|
||||||
assert.deepStrictEqual(just(3).isJust(), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has the inverse called get', () => {
|
|
||||||
assert.deepStrictEqual(just(3).get(), 3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('nothing', () => {
|
|
||||||
it('has no value', () => {
|
|
||||||
assert.deepStrictEqual(nothing().isJust(), false);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "13.0.0-preview1",
|
"version": "13.0.0-preview2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite build --watch --mode development",
|
"watch": "vite build --watch --mode development",
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
|
<div class="xfbouadm" :style="{ backgroundImage: `url(${ instance.backgroundImageUrl })` }"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { instance } from '@/instance';
|
||||||
import * as foundkey from 'foundkey-js';
|
|
||||||
import * as os from '@/os';
|
|
||||||
|
|
||||||
const meta = ref<foundkey.entities.DetailedInstanceMetadata>();
|
|
||||||
|
|
||||||
os.api('meta', { detail: true }).then(gotMeta => {
|
|
||||||
meta.value = gotMeta;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -34,15 +34,15 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const self = props.url.startsWith(local);
|
const self = props.url.startsWith(local);
|
||||||
const url = new URL(props.url);
|
const uri = new URL(props.url);
|
||||||
let el: HTMLElement | null = $ref(null);
|
let el: HTMLElement | null = $ref(null);
|
||||||
|
|
||||||
let schema = $ref(url.protocol);
|
let schema = $ref(uri.protocol);
|
||||||
let hostname = $ref(decodePunycode(url.hostname));
|
let hostname = $ref(decodePunycode(uri.hostname));
|
||||||
let port = $ref(url.port);
|
let port = $ref(uri.port);
|
||||||
let pathname = $ref(safeURIDecode(url.pathname));
|
let pathname = $ref(safeURIDecode(uri.pathname));
|
||||||
let query = $ref(safeURIDecode(url.search));
|
let query = $ref(safeURIDecode(uri.search));
|
||||||
let hash = $ref(safeURIDecode(url.hash));
|
let hash = $ref(safeURIDecode(uri.hash));
|
||||||
let attr = $ref(self ? 'to' : 'href');
|
let attr = $ref(self ? 'to' : 'href');
|
||||||
let target = $ref(self ? null : '_blank');
|
let target = $ref(self ? null : '_blank');
|
||||||
|
|
||||||
|
|
|
@ -750,7 +750,7 @@
|
||||||
{ "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] },
|
{ "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] },
|
||||||
{ "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] },
|
{ "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] },
|
||||||
{ "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] },
|
{ "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] },
|
||||||
{ "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] },
|
{ "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink", "juice", "straw"] },
|
||||||
{ "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] },
|
{ "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] },
|
||||||
{ "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] },
|
{ "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] },
|
||||||
{ "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] },
|
{ "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] },
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import * as foundkey from 'foundkey-js';
|
import * as foundkey from 'foundkey-js';
|
||||||
import { api } from '@/os';
|
import { apiGet } from '@/os';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
|
@ -13,13 +13,7 @@ export const instance: foundkey.entities.InstanceMetadata = reactive(instanceDat
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function fetchInstance(): Promise<void> {
|
export async function fetchInstance(): Promise<void> {
|
||||||
const meta = await api('meta', {
|
Object.assign(instance, await apiGet('meta'));
|
||||||
detail: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(meta)) {
|
|
||||||
instance[k] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('instance', JSON.stringify(instance));
|
localStorage.setItem('instance', JSON.stringify(instance));
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,7 +247,7 @@ export function inputText(props: {
|
||||||
input: {
|
input: {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
default: props.default,
|
default: props.default ?? '',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div v-show="loaded" class="mjndxjch">
|
<div v-show="loaded" class="mjndxjch">
|
||||||
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
|
||||||
<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
|
<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
|
||||||
<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
|
<p v-if="version === instance.version">{{ i18n.ts.pageLoadErrorDescription }}</p>
|
||||||
<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
|
<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
|
<p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
import * as foundkey from 'foundkey-js';
|
import * as foundkey from 'foundkey-js';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import { version } from '@/config';
|
import { version } from '@/config';
|
||||||
|
import { instance } from '@/instance';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@ -34,15 +35,11 @@ withDefaults(defineProps<{
|
||||||
|
|
||||||
let loaded = $ref(false);
|
let loaded = $ref(false);
|
||||||
let serverIsDead = $ref(false);
|
let serverIsDead = $ref(false);
|
||||||
let meta = $ref<foundkey.entities.LiteInstanceMetadata | null>(null);
|
|
||||||
|
|
||||||
os.api('meta', {
|
// just checking whether the server is alive or dead
|
||||||
detail: false,
|
os.api('ping').then(() => {
|
||||||
}).then(res => {
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
serverIsDead = false;
|
serverIsDead = false;
|
||||||
meta = res;
|
|
||||||
localStorage.setItem('v', res.version);
|
|
||||||
}, () => {
|
}, () => {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
serverIsDead = true;
|
serverIsDead = true;
|
||||||
|
|
|
@ -10,21 +10,22 @@
|
||||||
<template #label>{{ i18n.ts.search }}</template>
|
<template #label>{{ i18n.ts.search }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||||
<template #label>Select mode</template>
|
<template #label>{{ i18n.ts.selectMode }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
<MkButton inline @click="selectAll">{{ i18n.ts.selectAll }}</MkButton>
|
||||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
<MkButton inline @click="setCategoryBulk">{{ i18n.ts.setCategory }}</MkButton>
|
||||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
<MkButton inline @click="addTagBulk">{{ i18n.ts.addTag }}</MkButton>
|
||||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
<MkButton inline @click="removeTagBulk">{{ i18n.ts.removeTag }}</MkButton>
|
||||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
<MkButton inline @click="setTagBulk">{{ i18n.ts.setTag }}</MkButton>
|
||||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
<MkButton inline @click="exportSelected">{{ i18n.ts.exportSelected }}</MkButton>
|
||||||
|
<MkButton inline danger @click="delBulk">{{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div class="ldhfsamy">
|
<div class="ldhfsamy">
|
||||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectMode && selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name _monospace">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
|
@ -170,7 +171,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
|
||||||
const menu = (ev: MouseEvent) => {
|
const menu = (ev: MouseEvent) => {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
icon: 'fas fa-download',
|
icon: 'fas fa-download',
|
||||||
text: i18n.ts.export,
|
text: i18n.ts.exportAll,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
os.api('export-custom-emojis', {
|
os.api('export-custom-emojis', {
|
||||||
})
|
})
|
||||||
|
@ -257,6 +258,23 @@ const setTagBulk = async () => {
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportSelected = async () => {
|
||||||
|
os.api('export-custom-emojis', {
|
||||||
|
ids: selectedEmojis.value,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
os.alert({
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.ts.exportRequested,
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const delBulk = async () => {
|
const delBulk = async () => {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<XDrive ref="drive" @cd="x => folder = x"/>
|
<XDrive @cd="x => folder = x"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
|
<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
|
||||||
<template #label>CSS</template>
|
<template #label>CSS</template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
|
|
||||||
|
<a href="https://github.com/JakeMBauer/Misskey-Extras/tree/master/custom-css">{{ i18n.ts.externalCssSnippets }}</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta" class="rsqzvsbo">
|
<div class="rsqzvsbo">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<MkFeaturedPhotos class="bg"/>
|
<MkFeaturedPhotos class="bg"/>
|
||||||
<XTimeline class="tl"/>
|
<XTimeline class="tl"/>
|
||||||
|
@ -19,12 +19,12 @@
|
||||||
<div class="fg">
|
<div class="fg">
|
||||||
<h1>
|
<h1>
|
||||||
<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
|
<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
|
||||||
<!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
|
<!-- <img class="logo" v-if="instance.logoImageUrl" :src="instance.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
|
||||||
<span class="text">{{ instanceName }}</span>
|
<span class="text">{{ instanceName }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="about">
|
<div class="about">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div class="desc" v-html="meta.description || i18n.ts.headlineMisskey"></div>
|
<div class="desc" v-html="instance.description || i18n.ts.headlineMisskey"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<MkButton inline rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.signup }}</MkButton>
|
<MkButton inline rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.signup }}</MkButton>
|
||||||
|
@ -73,17 +73,13 @@ import { host, instanceName } from '@/config';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
|
||||||
let meta = $ref();
|
|
||||||
let stats = $ref();
|
let stats = $ref();
|
||||||
let tags = $ref();
|
let tags = $ref();
|
||||||
let onlineUsersCount = $ref();
|
let onlineUsersCount = $ref();
|
||||||
let instances = $ref();
|
let instances = $ref();
|
||||||
|
|
||||||
os.api('meta', { detail: true }).then(_meta => {
|
|
||||||
meta = _meta;
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('stats').then(_stats => {
|
os.api('stats').then(_stats => {
|
||||||
stats = _stats;
|
stats = _stats;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="meta">
|
<XSetup v-if="instance.requireSetup"/>
|
||||||
<XSetup v-if="meta.requireSetup"/>
|
|
||||||
<XEntrance v-else/>
|
<XEntrance v-else/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -10,15 +8,10 @@ import { computed } from 'vue';
|
||||||
import XSetup from './welcome.setup.vue';
|
import XSetup from './welcome.setup.vue';
|
||||||
import XEntrance from './welcome.entrance.a.vue';
|
import XEntrance from './welcome.entrance.a.vue';
|
||||||
import { instanceName } from '@/config';
|
import { instanceName } from '@/config';
|
||||||
|
import { instance } from '@/instance';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
let meta = $ref(null);
|
|
||||||
|
|
||||||
os.api('meta', { detail: true }).then(res => {
|
|
||||||
meta = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: instanceName,
|
title: instanceName,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
|
|
@ -80,7 +80,6 @@ const announcements = {
|
||||||
let showMenu = $ref(false);
|
let showMenu = $ref(false);
|
||||||
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD);
|
||||||
let narrow = $ref(window.innerWidth < 1280);
|
let narrow = $ref(window.innerWidth < 1280);
|
||||||
let meta = $ref();
|
|
||||||
|
|
||||||
const keymap = $computed(() => {
|
const keymap = $computed(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -94,10 +93,6 @@ const keymap = $computed(() => {
|
||||||
|
|
||||||
const root = $computed(() => mainRouter.currentRoute.value.name === 'index');
|
const root = $computed(() => mainRouter.currentRoute.value.name === 'index');
|
||||||
|
|
||||||
os.api('meta', { detail: true }).then(res => {
|
|
||||||
meta = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
function signin() {
|
function signin() {
|
||||||
os.popup(XSigninDialog, {
|
os.popup(XSigninDialog, {
|
||||||
autoSet: true,
|
autoSet: true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "foundkey-js",
|
"name": "foundkey-js",
|
||||||
"version": "13.0.0-preview1",
|
"version": "13.0.0-preview2",
|
||||||
"description": "Fork of misskey-js for Foundkey",
|
"description": "Fork of misskey-js for Foundkey",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sw",
|
"name": "sw",
|
||||||
"version": "13.0.0-preview1",
|
"version": "13.0.0-preview2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "node build.js watch",
|
"watch": "node build.js watch",
|
||||||
|
|
Loading…
Reference in a new issue