forked from FoundKeyGang/FoundKey
Compare commits
53 commits
f6a5459f91
...
04d4dd323f
Author | SHA1 | Date | |
---|---|---|---|
Norm | 04d4dd323f | ||
Norm | e814fdc7d1 | ||
Norm | f2f547172e | ||
Norm | f17485d8a2 | ||
Norm | 70eec26b74 | ||
Johann150 | a74c1d9126 | ||
Chloe Kudryavtsev | 811d5cd0d7 | ||
Chloe Kudryavtsev | d762143b89 | ||
Chloe Kudryavtsev | 21c1e5c06c | ||
Chloe Kudryavtsev | 91a4f38871 | ||
Johann150 | 756ecbb1f7 | ||
Johann150 | b431471fd1 | ||
Johann150 | 48023a0814 | ||
Johann150 | 8721e8844a | ||
Johann150 | 4b98677141 | ||
Johann150 | 177f34b02e | ||
Johann150 | 79c70c1017 | ||
Johann150 | cb0b14ba2d | ||
Johann150 | 7ea6deb19b | ||
Johann150 | 6d0cfe42f2 | ||
Johann150 | 7cd11e7afd | ||
Johann150 | 0b8fa2665c | ||
Johann150 | 421b42d07d | ||
Norm | 9a503273fb | ||
Norm | 20a6140e9a | ||
Norm | c29e24c103 | ||
Chloe Kudryavtsev | 6d58d5ed3b | ||
Chloe Kudryavtsev | 897658c188 | ||
Chloe Kudryavtsev | 590a8b98d9 | ||
Chloe Kudryavtsev | dd5100d124 | ||
Chloe Kudryavtsev | 3d9df839a5 | ||
Hélène | c414f24a2c | ||
Johann150 | 2c411d59f4 | ||
Michcio | 60600729df | ||
Norm | 683b0cfa82 | ||
Norm | c65559872b | ||
Norm | 02079593d5 | ||
Norm | e8ed254e4d | ||
Norm | 08c65e9797 | ||
Norm | 13a3581817 | ||
Johann150 | 808c43377e | ||
Johann150 | 8920eeb86a | ||
Norm | 91c043689b | ||
Michcio | 30faeb73d2 | ||
Michcio | dc510c6834 | ||
Michcio | a91bbed34e | ||
Johann150 | 4f9504d135 | ||
Johann150 | 9022ab9f2a | ||
Norm | 4e0f14c0db | ||
Johann150 | b018b05c5c | ||
Johann150 | b95f90eb15 | ||
Johann150 | 60e49a4196 | ||
Johann150 | 3f59b7f705 |
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\
|
||||||
|
@ -121,6 +123,8 @@ unmarkAsSensitive: "Unmark as NSFW"
|
||||||
enterFileName: "Enter filename"
|
enterFileName: "Enter filename"
|
||||||
mute: "Mute"
|
mute: "Mute"
|
||||||
unmute: "Unmute"
|
unmute: "Unmute"
|
||||||
|
renoteMute: "Hide renotes"
|
||||||
|
renoteUnmute: "Show renotes"
|
||||||
block: "Block"
|
block: "Block"
|
||||||
unblock: "Unblock"
|
unblock: "Unblock"
|
||||||
suspend: "Suspend"
|
suspend: "Suspend"
|
||||||
|
@ -918,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"
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
|
||||||
|
export class addRenoteMuting1665091090561 {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'addRenoteMuting1665091090561';
|
||||||
|
}
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "renote_muting"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { Meta } from '@/models/entities/meta.js';
|
||||||
import { Following } from '@/models/entities/following.js';
|
import { Following } from '@/models/entities/following.js';
|
||||||
import { Instance } from '@/models/entities/instance.js';
|
import { Instance } from '@/models/entities/instance.js';
|
||||||
import { Muting } from '@/models/entities/muting.js';
|
import { Muting } from '@/models/entities/muting.js';
|
||||||
|
import { RenoteMuting } from '@/models/entities/renote-muting.js';
|
||||||
import { SwSubscription } from '@/models/entities/sw-subscription.js';
|
import { SwSubscription } from '@/models/entities/sw-subscription.js';
|
||||||
import { Blocking } from '@/models/entities/blocking.js';
|
import { Blocking } from '@/models/entities/blocking.js';
|
||||||
import { UserList } from '@/models/entities/user-list.js';
|
import { UserList } from '@/models/entities/user-list.js';
|
||||||
|
@ -130,6 +131,7 @@ export const entities = [
|
||||||
Following,
|
Following,
|
||||||
FollowRequest,
|
FollowRequest,
|
||||||
Muting,
|
Muting,
|
||||||
|
RenoteMuting,
|
||||||
Blocking,
|
Blocking,
|
||||||
Note,
|
Note,
|
||||||
NoteFavorite,
|
NoteFavorite,
|
||||||
|
|
|
@ -7,8 +7,8 @@ import * as crypto from 'node:crypto';
|
||||||
const TIME2000 = 946684800000;
|
const TIME2000 = 946684800000;
|
||||||
let counter = crypto.randomBytes(2).readUInt16LE(0);
|
let counter = crypto.randomBytes(2).readUInt16LE(0);
|
||||||
|
|
||||||
export function genId(date?: Date = new Date()): string {
|
export function genId(date: Date = new Date()): string {
|
||||||
let t = Math.min(date, new Date());
|
let t = Math.min(date.valueOf(), new Date().valueOf());
|
||||||
t -= TIME2000;
|
t -= TIME2000;
|
||||||
if (t < 0) t = 0;
|
if (t < 0) t = 0;
|
||||||
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
|
||||||
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
|
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
|
||||||
import { packedFollowingSchema } from '@/models/schema/following.js';
|
import { packedFollowingSchema } from '@/models/schema/following.js';
|
||||||
import { packedMutingSchema } from '@/models/schema/muting.js';
|
import { packedMutingSchema } from '@/models/schema/muting.js';
|
||||||
|
import { packedRenoteMutingSchema } from '@/models/schema/renote-muting.js';
|
||||||
import { packedBlockingSchema } from '@/models/schema/blocking.js';
|
import { packedBlockingSchema } from '@/models/schema/blocking.js';
|
||||||
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
|
||||||
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
|
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
|
||||||
|
@ -51,6 +52,7 @@ export const refs = {
|
||||||
DriveFolder: packedDriveFolderSchema,
|
DriveFolder: packedDriveFolderSchema,
|
||||||
Following: packedFollowingSchema,
|
Following: packedFollowingSchema,
|
||||||
Muting: packedMutingSchema,
|
Muting: packedMutingSchema,
|
||||||
|
RenoteMuting: packedRenoteMutingSchema,
|
||||||
Blocking: packedBlockingSchema,
|
Blocking: packedBlockingSchema,
|
||||||
Hashtag: packedHashtagSchema,
|
Hashtag: packedHashtagSchema,
|
||||||
Page: packedPageSchema,
|
Page: packedPageSchema,
|
||||||
|
|
42
packages/backend/src/models/entities/renote-muting.ts
Normal file
42
packages/backend/src/models/entities/renote-muting.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
|
import { User } from './user.js';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['muterId', 'muteeId'], { unique: true })
|
||||||
|
export class RenoteMuting {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
comment: 'The created date of the Muting.',
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The mutee user ID.',
|
||||||
|
})
|
||||||
|
public muteeId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public mutee: User | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The muter user ID.',
|
||||||
|
})
|
||||||
|
public muterId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public muter: User | null;
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import { UserGroupJoining } from './entities/user-group-joining.js';
|
||||||
import { UserGroupInvitationRepository } from './repositories/user-group-invitation.js';
|
import { UserGroupInvitationRepository } from './repositories/user-group-invitation.js';
|
||||||
import { FollowRequestRepository } from './repositories/follow-request.js';
|
import { FollowRequestRepository } from './repositories/follow-request.js';
|
||||||
import { MutingRepository } from './repositories/muting.js';
|
import { MutingRepository } from './repositories/muting.js';
|
||||||
|
import { RenoteMutingRepository } from './repositories/renote-muting.js';
|
||||||
import { BlockingRepository } from './repositories/blocking.js';
|
import { BlockingRepository } from './repositories/blocking.js';
|
||||||
import { NoteReactionRepository } from './repositories/note-reaction.js';
|
import { NoteReactionRepository } from './repositories/note-reaction.js';
|
||||||
import { NotificationRepository } from './repositories/notification.js';
|
import { NotificationRepository } from './repositories/notification.js';
|
||||||
|
@ -95,6 +96,7 @@ export const DriveFolders = (DriveFolderRepository);
|
||||||
export const Notifications = (NotificationRepository);
|
export const Notifications = (NotificationRepository);
|
||||||
export const Metas = db.getRepository(Meta);
|
export const Metas = db.getRepository(Meta);
|
||||||
export const Mutings = (MutingRepository);
|
export const Mutings = (MutingRepository);
|
||||||
|
export const RenoteMutings = (RenoteMutingRepository);
|
||||||
export const Blockings = (BlockingRepository);
|
export const Blockings = (BlockingRepository);
|
||||||
export const SwSubscriptions = db.getRepository(SwSubscription);
|
export const SwSubscriptions = db.getRepository(SwSubscription);
|
||||||
export const Hashtags = (HashtagRepository);
|
export const Hashtags = (HashtagRepository);
|
||||||
|
|
31
packages/backend/src/models/repositories/renote-muting.ts
Normal file
31
packages/backend/src/models/repositories/renote-muting.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { db } from '@/db/postgre.js';
|
||||||
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
import { RenoteMuting } from '@/models/entities/renote-muting.js';
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
|
import { Users } from '../index.js';
|
||||||
|
|
||||||
|
export const RenoteMutingRepository = db.getRepository(RenoteMuting).extend({
|
||||||
|
async pack(
|
||||||
|
src: RenoteMuting['id'] | RenoteMuting,
|
||||||
|
me?: { id: User['id'] } | null | undefined,
|
||||||
|
): Promise<Packed<'RenoteMuting'>> {
|
||||||
|
const muting = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
return await awaitAll({
|
||||||
|
id: muting.id,
|
||||||
|
createdAt: muting.createdAt.toISOString(),
|
||||||
|
muteeId: muting.muteeId,
|
||||||
|
mutee: Users.pack(muting.muteeId, me, {
|
||||||
|
detail: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
packMany(
|
||||||
|
mutings: any[],
|
||||||
|
me: { id: User['id'] },
|
||||||
|
) {
|
||||||
|
return Promise.all(mutings.map(x => this.pack(x, me)));
|
||||||
|
},
|
||||||
|
});
|
|
@ -10,7 +10,7 @@ import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Instance } from '../entities/instance.js';
|
import { Instance } from '../entities/instance.js';
|
||||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.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>(1000 * 60 * 60 * 3);
|
||||||
|
|
||||||
|
@ -112,6 +112,13 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
},
|
},
|
||||||
take: 1,
|
take: 1,
|
||||||
}).then(n => n > 0),
|
}).then(n => n > 0),
|
||||||
|
isRenoteMuted: RenoteMutings.count({
|
||||||
|
where: {
|
||||||
|
muterId: me,
|
||||||
|
muteeId: target,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
}).then(n => n > 0),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -412,6 +419,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
isBlocking: relation.isBlocking,
|
isBlocking: relation.isBlocking,
|
||||||
isBlocked: relation.isBlocked,
|
isBlocked: relation.isBlocked,
|
||||||
isMuted: relation.isMuted,
|
isMuted: relation.isMuted,
|
||||||
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||||
|
|
||||||
|
|
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
export const packedRenoteMutingSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
example: 'xxxxxxxxxx',
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
muteeId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
mutee: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'UserDetailed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -263,6 +263,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -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" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { RenoteMutings } from '@/models/index.js';
|
||||||
|
|
||||||
|
export function generateMutedRenotesQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
|
||||||
|
const mutingQuery = RenoteMutings.createQueryBuilder('renote_muting')
|
||||||
|
.select('renote_muting.muteeId')
|
||||||
|
.where('renote_muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
|
q.andWhere(new Brackets(qb => {
|
||||||
|
qb
|
||||||
|
.where(new Brackets(qb => {
|
||||||
|
qb.where('note.renoteId IS NOT NULL');
|
||||||
|
qb.andWhere('note.text IS NULL');
|
||||||
|
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
|
}))
|
||||||
|
.orWhere('note.renoteId IS NULL')
|
||||||
|
.orWhere('note.text IS NOT NULL');
|
||||||
|
}));
|
||||||
|
|
||||||
|
q.setParameters(mutingQuery.getParameters());
|
||||||
|
}
|
|
@ -214,6 +214,9 @@ import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||||
import * as ep___mute_list from './endpoints/mute/list.js';
|
import * as ep___mute_list from './endpoints/mute/list.js';
|
||||||
|
import * as ep___renote_mute_create from './endpoints/renote-mute/create.js';
|
||||||
|
import * as ep___renote_mute_delete from './endpoints/renote-mute/delete.js';
|
||||||
|
import * as ep___renote_mute_list from './endpoints/renote-mute/list.js';
|
||||||
import * as ep___my_apps from './endpoints/my/apps.js';
|
import * as ep___my_apps from './endpoints/my/apps.js';
|
||||||
import * as ep___notes from './endpoints/notes.js';
|
import * as ep___notes from './endpoints/notes.js';
|
||||||
import * as ep___notes_children from './endpoints/notes/children.js';
|
import * as ep___notes_children from './endpoints/notes/children.js';
|
||||||
|
@ -521,6 +524,9 @@ const eps = [
|
||||||
['mute/create', ep___mute_create],
|
['mute/create', ep___mute_create],
|
||||||
['mute/delete', ep___mute_delete],
|
['mute/delete', ep___mute_delete],
|
||||||
['mute/list', ep___mute_list],
|
['mute/list', ep___mute_list],
|
||||||
|
['renote-mute/create', ep___renote_mute_create],
|
||||||
|
['renote-mute/delete', ep___renote_mute_delete],
|
||||||
|
['renote-mute/list', ep___renote_mute_list],
|
||||||
['my/apps', ep___my_apps],
|
['my/apps', ep___my_apps],
|
||||||
['notes', ep___notes],
|
['notes', ep___notes],
|
||||||
['notes/children', ep___notes_children],
|
['notes/children', ep___notes_children],
|
||||||
|
|
|
@ -87,4 +87,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
return await Users.pack(blockee.id, blocker, {
|
return await Users.pack(blockee.id, blocker, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
publishUserEvent(user.id, 'block', blockee);
|
||||||
});
|
});
|
||||||
|
|
|
@ -83,4 +83,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
return await Users.pack(blockee.id, blocker, {
|
return await Users.pack(blockee.id, blocker, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
publishUserEvent(user.id, 'unblock', blockee);
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
parentId: parent !== null ? parent.id : null,
|
parentId: parent?.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}).then(x => DriveFolders.findOneByOrFail(x.identifiers[0]));
|
}).then(x => DriveFolders.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query.j
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
|
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -79,6 +80,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
generateMutedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
generateMutedNoteQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
generateBlockedUserQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
generateMutedRenotesQuery(query, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
|
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -93,6 +94,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
generateMutedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
generateMutedNoteQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
generateBlockedUserQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
generateMutedRenotesQuery(query, user);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
|
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -86,6 +87,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (user) generateMutedUserQuery(query, user);
|
if (user) generateMutedUserQuery(query, user);
|
||||||
if (user) generateMutedNoteQuery(query, user);
|
if (user) generateMutedNoteQuery(query, user);
|
||||||
if (user) generateBlockedUserQuery(query, user);
|
if (user) generateBlockedUserQuery(query, user);
|
||||||
|
if (user) generateMutedRenotesQuery(query, user);
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
|
import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -85,6 +86,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
generateMutedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
generateMutedNoteQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
generateBlockedUserQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
generateMutedRenotesQuery(query, user);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { genId } from '@/misc/gen-id.js';
|
||||||
|
import { RenoteMutings } from '@/models/index.js';
|
||||||
|
import { RenoteMuting } from '@/models/entities/renote-muting.js';
|
||||||
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
import { getUser } from '../../common/getters.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:mutes',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '6fef56f3-e765-4957-88e5-c6f65329b8a5',
|
||||||
|
},
|
||||||
|
|
||||||
|
muteeIsYourself: {
|
||||||
|
message: 'Mutee is yourself.',
|
||||||
|
code: 'MUTEE_IS_YOURSELF',
|
||||||
|
id: 'a4619cb2-5f23-484b-9301-94c903074e10',
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyMuting: {
|
||||||
|
message: 'You are already muting that user.',
|
||||||
|
code: 'ALREADY_MUTING',
|
||||||
|
id: '7e7359cb-160c-4956-b08f-4d1c653cd007',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const muter = user;
|
||||||
|
|
||||||
|
// Check if the mutee is yourself
|
||||||
|
if (user.id === ps.userId) {
|
||||||
|
throw new ApiError(meta.errors.muteeIsYourself);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mutee
|
||||||
|
const mutee = await getUser(ps.userId).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if already muting
|
||||||
|
const exist = await RenoteMutings.findOneBy({
|
||||||
|
muterId: muter.id,
|
||||||
|
muteeId: mutee.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyMuting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mute
|
||||||
|
await RenoteMutings.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
muterId: muter.id,
|
||||||
|
muteeId: mutee.id,
|
||||||
|
} as RenoteMuting);
|
||||||
|
|
||||||
|
publishUserEvent(user.id, 'muteRenote', mutee);
|
||||||
|
});
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { RenoteMutings } from '@/models/index.js';
|
||||||
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
import { getUser } from '../../common/getters.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:mutes',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef',
|
||||||
|
},
|
||||||
|
|
||||||
|
muteeIsYourself: {
|
||||||
|
message: 'Mutee is yourself.',
|
||||||
|
code: 'MUTEE_IS_YOURSELF',
|
||||||
|
id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9',
|
||||||
|
},
|
||||||
|
|
||||||
|
notMuting: {
|
||||||
|
message: 'You are not muting that user.',
|
||||||
|
code: 'NOT_MUTING',
|
||||||
|
id: '5467d020-daa9-4553-81e1-135c0c35a96d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const muter = user;
|
||||||
|
|
||||||
|
// Check if the mutee is yourself
|
||||||
|
if (user.id === ps.userId) {
|
||||||
|
throw new ApiError(meta.errors.muteeIsYourself);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mutee
|
||||||
|
const mutee = await getUser(ps.userId).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check not muting
|
||||||
|
const exist = await RenoteMutings.findOneBy({
|
||||||
|
muterId: muter.id,
|
||||||
|
muteeId: mutee.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist == null) {
|
||||||
|
throw new ApiError(meta.errors.notMuting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete mute
|
||||||
|
await RenoteMutings.delete({
|
||||||
|
id: exist.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
publishUserEvent(user.id, 'unmuteRenote', mutee);
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { RenoteMutings } from '@/models/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:mutes',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'RenoteMuting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = makePaginationQuery(RenoteMutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere('muting.muterId = :meId', { meId: me.id });
|
||||||
|
|
||||||
|
const mutings = await query
|
||||||
|
.take(ps.limit)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return await RenoteMutings.packMany(mutings, me);
|
||||||
|
});
|
|
@ -47,6 +47,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -88,6 +92,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { publishAdminStream } from '@/services/stream.js';
|
import { publishAdminStream } from '@/services/stream.js';
|
||||||
import { AbuseUserReports, Users } from '@/models/index.js';
|
import { AbuseUserReports, Users } from '@/models/index.js';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Note } from '@/models/entities/note.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import Connection from '.';
|
import Connection from './index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream channel
|
* Stream channel
|
||||||
|
@ -30,6 +30,10 @@ export default abstract class Channel {
|
||||||
return this.connection.muting;
|
return this.connection.muting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get renoteMuting() {
|
||||||
|
return this.connection.renoteMuting;
|
||||||
|
}
|
||||||
|
|
||||||
protected get blocking() {
|
protected get blocking() {
|
||||||
return this.connection.blocking;
|
return this.connection.blocking;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -49,6 +49,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -40,6 +40,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -55,6 +55,7 @@ export default class extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
if (note.renote && this.renoteMuting.has(note.userId)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as websocket from 'websocket';
|
||||||
import readNote from '@/services/note/read.js';
|
import readNote from '@/services/note/read.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { Channel as ChannelModel } from '@/models/entities/channel.js';
|
import { Channel as ChannelModel } from '@/models/entities/channel.js';
|
||||||
import { Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
|
||||||
import { AccessToken } from '@/models/entities/access-token.js';
|
import { AccessToken } from '@/models/entities/access-token.js';
|
||||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||||
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js';
|
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js';
|
||||||
|
@ -22,6 +22,7 @@ export default class Connection {
|
||||||
public userProfile?: UserProfile | null;
|
public userProfile?: UserProfile | null;
|
||||||
public following: Set<User['id']> = new Set();
|
public following: Set<User['id']> = new Set();
|
||||||
public muting: Set<User['id']> = new Set();
|
public muting: Set<User['id']> = new Set();
|
||||||
|
public renoteMuting: Set<User['id']> = new Set();
|
||||||
public blocking: Set<User['id']> = new Set(); // "被"blocking
|
public blocking: Set<User['id']> = new Set(); // "被"blocking
|
||||||
public followingChannels: Set<ChannelModel['id']> = new Set();
|
public followingChannels: Set<ChannelModel['id']> = new Set();
|
||||||
public token?: AccessToken;
|
public token?: AccessToken;
|
||||||
|
@ -56,6 +57,7 @@ export default class Connection {
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
this.updateFollowing();
|
this.updateFollowing();
|
||||||
this.updateMuting();
|
this.updateMuting();
|
||||||
|
this.updateRenoteMuting();
|
||||||
this.updateBlocking();
|
this.updateBlocking();
|
||||||
this.updateFollowingChannels();
|
this.updateFollowingChannels();
|
||||||
this.updateUserProfile();
|
this.updateUserProfile();
|
||||||
|
@ -82,7 +84,21 @@ export default class Connection {
|
||||||
this.muting.delete(data.body.id);
|
this.muting.delete(data.body.id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// TODO: block events
|
case 'block':
|
||||||
|
this.blocking.add(data.body.id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'unblock':
|
||||||
|
this.blocking.delete(data.body.id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'muteRenote':
|
||||||
|
this.renoteMuting.add(data.body.id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'unmuteRenote':
|
||||||
|
this.renoteMuting.delete(data.body.id);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'followChannel':
|
case 'followChannel':
|
||||||
this.followingChannels.add(data.body.id);
|
this.followingChannels.add(data.body.id);
|
||||||
|
@ -333,6 +349,17 @@ export default class Connection {
|
||||||
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateRenoteMuting() {
|
||||||
|
const renoteMutings = await RenoteMutings.find({
|
||||||
|
where: {
|
||||||
|
muterId: this.user!.id,
|
||||||
|
},
|
||||||
|
select: ['muteeId'],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renoteMuting = new Set<string>(renoteMutings.map(x => x.muteeId));
|
||||||
|
}
|
||||||
|
|
||||||
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
||||||
const blockings = await Blockings.find({
|
const blockings = await Blockings.find({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
|
||||||
import { Signin } from '@/models/entities/signin.js';
|
import { Signin } from '@/models/entities/signin.js';
|
||||||
import { Page } from '@/models/entities/page.js';
|
import { Page } from '@/models/entities/page.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
import { Webhook } from '@/models/entities/webhook';
|
import { Webhook } from '@/models/entities/webhook.js';
|
||||||
|
|
||||||
//#region Stream type-body definitions
|
//#region Stream type-body definitions
|
||||||
export interface InternalStreamTypes {
|
export interface InternalStreamTypes {
|
||||||
|
@ -44,6 +44,10 @@ export interface UserStreamTypes {
|
||||||
updateUserProfile: UserProfile;
|
updateUserProfile: UserProfile;
|
||||||
mute: User;
|
mute: User;
|
||||||
unmute: User;
|
unmute: User;
|
||||||
|
muteRenote: User;
|
||||||
|
unmuteRenote: User;
|
||||||
|
block: User;
|
||||||
|
unblock: User;
|
||||||
follow: Packed<'UserDetailedNotMe'>;
|
follow: Packed<'UserDetailedNotMe'>;
|
||||||
unfollow: Packed<'User'>;
|
unfollow: Packed<'User'>;
|
||||||
userAdded: Packed<'User'>;
|
userAdded: Packed<'User'>;
|
||||||
|
|
|
@ -64,9 +64,6 @@ html
|
||||||
var VERSION = "#{version}";
|
var VERSION = "#{version}";
|
||||||
var CLIENT_ENTRY = "#{clientEntry.file}";
|
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||||
|
|
||||||
script
|
|
||||||
include ../boot.js
|
|
||||||
|
|
||||||
body
|
body
|
||||||
noscript: p
|
noscript: p
|
||||||
| JavaScriptを有効にしてください
|
| JavaScriptを有効にしてください
|
||||||
|
@ -86,3 +83,6 @@ html
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
block content
|
block content
|
||||||
|
|
||||||
|
script
|
||||||
|
include ../boot.js
|
||||||
|
|
|
@ -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 !== null ? folder.id : null;
|
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",
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"broadcast-channel": "4.13.0",
|
"broadcast-channel": "4.13.0",
|
||||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
|
"browser-image-resizer": "2.4.1",
|
||||||
"chart.js": "3.8.0",
|
"chart.js": "3.8.0",
|
||||||
"chartjs-adapter-date-fns": "2.0.0",
|
"chartjs-adapter-date-fns": "2.0.0",
|
||||||
"chartjs-plugin-gradient": "0.5.0",
|
"chartjs-plugin-gradient": "0.5.0",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
@drop.stop="onDrop"
|
@drop.stop="onDrop"
|
||||||
>
|
>
|
||||||
<i v-if="folder == null" class="fas fa-cloud"></i>
|
<i v-if="folder == null" class="fas fa-cloud"></i>
|
||||||
<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
|
<span>{{ folder?.name ?? i18n.ts.drive }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -117,7 +117,7 @@ function showTabsPopup(ev: MouseEvent): void {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||||
};
|
}
|
||||||
|
|
||||||
function preventDrag(ev: TouchEvent): void {
|
function preventDrag(ev: TouchEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const menu = defaultStore.state.menu;
|
const menu = defaultStore.state.menu;
|
||||||
|
|
||||||
const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuDef[k]).filter(def => def.show ?? true).map(def => ({
|
||||||
type: def.to ? 'link' : 'button',
|
type: def.to ? 'link' : 'button',
|
||||||
text: i18n.ts[def.title],
|
text: i18n.ts[def.title],
|
||||||
icon: def.icon,
|
icon: def.icon,
|
||||||
|
|
|
@ -1,104 +1,75 @@
|
||||||
<script lang="ts">
|
<template>
|
||||||
import { h, onMounted, onUnmounted, ref, watch } from 'vue';
|
<div class="marquee">
|
||||||
|
<span ref="contentEl" :class="{ content: true, paused, reverse }">
|
||||||
|
<span v-for="i in repeat" :key="i" class="text">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
export default {
|
<script lang="ts" setup>
|
||||||
name: 'MarqueeText',
|
import { watch, onMounted } from 'vue';
|
||||||
props: {
|
|
||||||
duration: {
|
|
||||||
type: Number,
|
|
||||||
default: 15,
|
|
||||||
},
|
|
||||||
repeat: {
|
|
||||||
type: Number,
|
|
||||||
default: 2,
|
|
||||||
},
|
|
||||||
paused: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
reverse: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const contentEl = ref();
|
|
||||||
|
|
||||||
function calc() {
|
const props = withDefaults(defineProps<{
|
||||||
const eachLength = contentEl.value.offsetWidth / props.repeat;
|
duration?: number;
|
||||||
const factor = 3000;
|
repeat?: number;
|
||||||
const duration = props.duration / ((1 / eachLength) * factor);
|
paused?: boolean;
|
||||||
|
reverse?: boolean;
|
||||||
|
}>(), {
|
||||||
|
duration: 15,
|
||||||
|
repeat: 2,
|
||||||
|
paused: false,
|
||||||
|
reverse: false,
|
||||||
|
});
|
||||||
|
|
||||||
contentEl.value.style.animationDuration = `${duration}s`;
|
let contentEl: HTMLElement = $ref();
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.duration, calc);
|
function calc(): void {
|
||||||
|
const eachLength = contentEl.offsetWidth / props.repeat;
|
||||||
|
const factor = 3000;
|
||||||
|
const duration = props.duration / ((1 / eachLength) * factor);
|
||||||
|
|
||||||
onMounted(() => {
|
contentEl.style.animationDuration = `${duration}s`;
|
||||||
calc();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
watch(() => props.duration, calc);
|
||||||
});
|
onMounted(calc);
|
||||||
|
|
||||||
return {
|
|
||||||
contentEl,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render({
|
|
||||||
$slots, $style, $props: {
|
|
||||||
repeat, paused, reverse,
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
return h('div', { class: [$style.wrap] }, [
|
|
||||||
h('span', {
|
|
||||||
ref: 'contentEl',
|
|
||||||
class: [
|
|
||||||
paused
|
|
||||||
? $style.paused
|
|
||||||
: undefined,
|
|
||||||
$style.content,
|
|
||||||
],
|
|
||||||
}, Array(repeat).fill(
|
|
||||||
h('span', {
|
|
||||||
class: $style.text,
|
|
||||||
style: {
|
|
||||||
animationDirection: reverse
|
|
||||||
? 'reverse'
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
}, $slots.default()),
|
|
||||||
)),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" scoped>
|
||||||
.wrap {
|
.marquee {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
animation-play-state: running;
|
animation-play-state: running;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation-play-state: inherit;
|
||||||
|
|
||||||
|
&.paused {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reverse {
|
||||||
|
animation-direction: reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: inline-block;
|
||||||
|
animation-name: marquee;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-duration: inherit;
|
||||||
|
animation-play-state: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.content {
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
animation-play-state: inherit;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
display: inline-block;
|
|
||||||
animation-name: marquee;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: inherit;
|
|
||||||
animation-play-state: inherit;
|
|
||||||
}
|
|
||||||
.paused .text {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
@keyframes marquee {
|
@keyframes marquee {
|
||||||
0% { transform:translateX(0); }
|
0% { transform:translateX(0); }
|
||||||
100% { transform:translateX(-100%); }
|
100% { transform:translateX(-100%); }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="pxhvhrfw" v-size="{ max: [500] }">
|
<div v-size="{ max: [500] }" class="pxhvhrfw">
|
||||||
<button
|
<button
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
|
@ -17,12 +17,12 @@ const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: string): void;
|
(ev: 'update:modelValue', value: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
options: {
|
options: {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
}[];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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 +1 @@
|
||||||
export default n => n == null ? 'N/A' : n.toLocaleString();
|
export default n => n?.toLocaleString() ?? 'N/A';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ async function edit(type) {
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
enum: soundsTypes.map(x => ({
|
enum: soundsTypes.map(x => ({
|
||||||
value: x,
|
value: x,
|
||||||
label: x == null ? i18n.ts.none : x,
|
label: x ?? i18n.ts.none,
|
||||||
})),
|
})),
|
||||||
label: i18n.ts.sound,
|
label: i18n.ts.sound,
|
||||||
default: sounds.value[type].type,
|
default: sounds.value[type].type,
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -58,17 +58,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,
|
||||||
|
|
|
@ -98,6 +98,14 @@ export function getUserMenu(user) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRenoteMute(): Promise<void> {
|
||||||
|
os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
|
||||||
|
userId: user.id,
|
||||||
|
}).then(() => {
|
||||||
|
user.isRenoteMuted = !user.isRenoteMuted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleBlock(): Promise<void> {
|
async function toggleBlock(): Promise<void> {
|
||||||
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
||||||
|
|
||||||
|
@ -187,6 +195,10 @@ export function getUserMenu(user) {
|
||||||
|
|
||||||
if ($i && meId !== user.id) {
|
if ($i && meId !== user.id) {
|
||||||
menu = menu.concat([null, {
|
menu = menu.concat([null, {
|
||||||
|
icon: user.isRenoteMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||||
|
text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
|
||||||
|
action: toggleRenoteMute,
|
||||||
|
}, {
|
||||||
icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
|
icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
|
||||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||||
action: toggleMute,
|
action: toggleMute,
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||||
|
|
||||||
export function getScrollPosition(el: Element | null): number {
|
export function getScrollPosition(el: Element | null): number {
|
||||||
const container = getScrollContainer(el);
|
const container = getScrollContainer(el);
|
||||||
return container == null ? window.scrollY : container.scrollTop;
|
return container?.scrollTop ?? window.scrollY;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTopVisible(el: Element | null): boolean {
|
export function isTopVisible(el: Element | null): boolean {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -411,6 +411,9 @@ export type Endpoints = {
|
||||||
'mute/create': { req: TODO; res: TODO; };
|
'mute/create': { req: TODO; res: TODO; };
|
||||||
'mute/delete': { req: { userId: User['id'] }; res: null; };
|
'mute/delete': { req: { userId: User['id'] }; res: null; };
|
||||||
'mute/list': { req: TODO; res: TODO; };
|
'mute/list': { req: TODO; res: TODO; };
|
||||||
|
'renote-mute/create': { req: TODO; res: TODO; };
|
||||||
|
'renote-mute/delete': { req: { userId: User['id'] }; res: null; };
|
||||||
|
'renote-mute/list': { req: TODO; res: TODO; };
|
||||||
'my/apps': { req: TODO; res: TODO; };
|
'my/apps': { req: TODO; res: TODO; };
|
||||||
'notes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
'notes': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
||||||
'notes/children': { req: { noteId: Note['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
'notes/children': { req: { noteId: Note['id']; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
||||||
|
|
|
@ -53,6 +53,7 @@ export type UserDetailed = UserLite & {
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isModerator: boolean;
|
isModerator: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
|
isRenoteMuted: boolean;
|
||||||
isSilenced: boolean;
|
isSilenced: boolean;
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
lang: string | null;
|
lang: string | null;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -3989,10 +3989,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"browser-image-resizer@git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2":
|
"browser-image-resizer@npm:2.4.1":
|
||||||
version: 2.2.1-misskey.2
|
version: 2.4.1
|
||||||
resolution: "browser-image-resizer@https://github.com/misskey-dev/browser-image-resizer.git#commit=a58834f5fe2af9f9f31ff115121aef3de6f9d416"
|
resolution: "browser-image-resizer@npm:2.4.1"
|
||||||
checksum: eb5ddfe7f6a2de96340ef420df9be03a75f2bfd8f568a60be22cc8f2ccdcb754105b7799cf706c09add3f9d82b7ce1c6f963842f80a85f234b26ef6bf1b8da09
|
checksum: b2f705696f2643240880314926f976b473452a7ef1335dcc888993281c8a8e57eba2cda255965843526511a206b722be0031df770bd31ddd944bc8730521ddb6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -4699,7 +4699,7 @@ __metadata:
|
||||||
autwh: 0.1.0
|
autwh: 0.1.0
|
||||||
blurhash: 1.1.5
|
blurhash: 1.1.5
|
||||||
broadcast-channel: 4.13.0
|
broadcast-channel: 4.13.0
|
||||||
browser-image-resizer: "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2"
|
browser-image-resizer: 2.4.1
|
||||||
chart.js: 3.8.0
|
chart.js: 3.8.0
|
||||||
chartjs-adapter-date-fns: 2.0.0
|
chartjs-adapter-date-fns: 2.0.0
|
||||||
chartjs-plugin-gradient: 0.5.0
|
chartjs-plugin-gradient: 0.5.0
|
||||||
|
|
Loading…
Reference in a new issue