Merge branch 'develop' of https://github.com/ThatOneCalculator/misskey into develop
This commit is contained in:
		
						commit
						5711d3bb41
					
				
					 41 changed files with 600 additions and 289 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/workflows/labeler.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/labeler.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,8 @@ | |||
| name: "Pull Request Labeler" | ||||
| on: | ||||
| - pull_request_target | ||||
|   pull_request_target: | ||||
|     branches-ignore: | ||||
|       - 'l10n_develop' | ||||
| 
 | ||||
| jobs: | ||||
|   triage: | ||||
|  |  | |||
							
								
								
									
										36
									
								
								.github/workflows/ok-to-test.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/ok-to-test.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| # If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event | ||||
| name: Ok To Test | ||||
| 
 | ||||
| on: | ||||
|   issue_comment: | ||||
|     types: [created] | ||||
| 
 | ||||
| jobs: | ||||
|   ok-to-test: | ||||
|     runs-on: ubuntu-latest | ||||
|     # Only run for PRs, not issue comments | ||||
|     if: ${{ github.event.issue.pull_request }} | ||||
|     steps: | ||||
|     # Generate a GitHub App installation access token from an App ID and private key | ||||
|     # To create a new GitHub App: | ||||
|     #   https://developer.github.com/apps/building-github-apps/creating-a-github-app/ | ||||
|     # See app.yml for an example app manifest | ||||
|     - name: Generate token | ||||
|       id: generate_token | ||||
|       uses: tibdex/github-app-token@v1 | ||||
|       with: | ||||
|         app_id: ${{ secrets.DEPLOYBOT_APP_ID }} | ||||
|         private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} | ||||
| 
 | ||||
|     - name: Slash Command Dispatch | ||||
|       uses: peter-evans/slash-command-dispatch@v1 | ||||
|       env: | ||||
|         TOKEN: ${{ steps.generate_token.outputs.token }} | ||||
|       with: | ||||
|         token: ${{ env.TOKEN }} # GitHub App installation access token | ||||
|         # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work | ||||
|         reaction-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         issue-type: pull-request | ||||
|         commands: deploy | ||||
|         named-args: true | ||||
|         permission: write | ||||
							
								
								
									
										95
									
								
								.github/workflows/pr-preview-deploy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								.github/workflows/pr-preview-deploy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| # Run secret-dependent integration tests only after /deploy approval | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [opened, reopened, synchronize] | ||||
|   repository_dispatch: | ||||
|     types: [deploy-command] | ||||
| 
 | ||||
| name: Deploy preview environment | ||||
| 
 | ||||
| jobs: | ||||
|   # Repo owner has commented /deploy on a (fork-based) pull request | ||||
|   deploy-preview-environment: | ||||
|     runs-on: ubuntu-latest | ||||
|     if: | ||||
|       github.event_name == 'repository_dispatch' && | ||||
|       github.event.client_payload.slash_command.sha != '' && | ||||
|       contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) | ||||
|     steps: | ||||
|     - uses: actions/github-script@v5 | ||||
|       id: check-id | ||||
|       env: | ||||
|         number: ${{ github.event.client_payload.pull_request.number }} | ||||
|         job: ${{ github.job }} | ||||
|       with: | ||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         result-encoding: string | ||||
|         script: | | ||||
|           const { data: pull } = await github.rest.pulls.get({ | ||||
|             ...context.repo, | ||||
|             pull_number: process.env.number | ||||
|           }); | ||||
|           const ref = pull.head.sha; | ||||
| 
 | ||||
|           const { data: checks } = await github.rest.checks.listForRef({ | ||||
|             ...context.repo, | ||||
|             ref | ||||
|           }); | ||||
| 
 | ||||
|           const check = checks.check_runs.filter(c => c.name === process.env.job); | ||||
| 
 | ||||
|           return check[0].id; | ||||
| 
 | ||||
|     - uses: actions/github-script@v5 | ||||
|       env: | ||||
|         check_id: ${{ steps.check-id.outputs.result }} | ||||
|         details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} | ||||
|       with: | ||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         script: | | ||||
|           await github.rest.checks.update({ | ||||
|             ...context.repo, | ||||
|             check_run_id: process.env.check_id, | ||||
|             status: 'in_progress', | ||||
|             details_url: process.env.details_url | ||||
|           }); | ||||
| 
 | ||||
|     # Check out merge commit | ||||
|     - name: Fork based /deploy checkout | ||||
|       uses: actions/checkout@v2 | ||||
|       with: | ||||
|         ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' | ||||
| 
 | ||||
|     # <insert integration tests needing secrets> | ||||
|     - name: Context | ||||
|       uses: okteto/context@latest | ||||
|       with: | ||||
|         token: ${{ secrets.OKTETO_TOKEN }} | ||||
| 
 | ||||
|     - name: Deploy preview environment | ||||
|       uses: ikuradon/deploy-preview@latest | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|       with: | ||||
|         name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo | ||||
|         timeout: 15m | ||||
| 
 | ||||
|     # Update check run called "integration-fork" | ||||
|     - uses: actions/github-script@v5 | ||||
|       id: update-check-run | ||||
|       if: ${{ always() }} | ||||
|       env: | ||||
|         # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run | ||||
|         conclusion: ${{ job.status }} | ||||
|         check_id: ${{ steps.check-id.outputs.result }} | ||||
|       with: | ||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         script: | | ||||
|           const { data: result } = await github.rest.checks.update({ | ||||
|             ...context.repo, | ||||
|             check_run_id: process.env.check_id, | ||||
|             status: 'completed', | ||||
|             conclusion: process.env.conclusion | ||||
|           }); | ||||
| 
 | ||||
|           return result; | ||||
							
								
								
									
										21
									
								
								.github/workflows/pr-preview-destroy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/pr-preview-destroy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| # file: .github/workflows/preview-closed.yaml | ||||
| on: | ||||
|   pull_request: | ||||
|     types: | ||||
|       - closed | ||||
| 
 | ||||
| name: Destroy preview environment | ||||
| 
 | ||||
| jobs: | ||||
|   destroy-preview-environment: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Context | ||||
|         uses: okteto/context@latest | ||||
|         with: | ||||
|           token: ${{ secrets.OKTETO_TOKEN }} | ||||
| 
 | ||||
|       - name: Destroy preview environment | ||||
|         uses: okteto/destroy-preview@latest | ||||
|         with: | ||||
|           name: pr-${{ github.event.number }}-syuilo | ||||
|  | @ -1 +1 @@ | |||
| v18.0.0 | ||||
| v16.15.0 | ||||
|  |  | |||
							
								
								
									
										46
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										46
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -10,18 +10,17 @@ You should also include the user name that made the change. | |||
| --> | ||||
| 
 | ||||
| ## 12.x.x (unreleased) | ||||
| ### NOTE | ||||
| - From this version, Node 18.0.0 or later is required. | ||||
| 
 | ||||
| ### Improvements | ||||
| - enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina | ||||
| - enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina | ||||
| - enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina | ||||
| - enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina | ||||
| - replaced webpack with Vite @tamaina | ||||
| - update dependencies @syuilo | ||||
| - enhance: display URL of QR code for TOTP registration @syuilo | ||||
| - enhance: Supports Unicode Emoji 14.0 @mei23 | ||||
| - Supports Unicode Emoji 14.0 @mei23 | ||||
| - プッシュ通知を複数アカウント対応に #7667 @tamaina | ||||
| - プッシュ通知にクリックやactionを設定 #7667 @tamaina | ||||
| - ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina | ||||
| - Server: always remove completed tasks of job queue @Johann150 | ||||
| - Client: make emoji stand out more on reaction button @Johann150 | ||||
| - Client: display URL of QR code for TOTP registration @tamaina | ||||
| - API: notifications/readは配列でも受け付けるように #7667 @tamaina | ||||
| - API: ユーザー検索で、クエリがusernameの条件を満たす場合はusernameもLIKE検索するように @tamaina | ||||
| - MFM: Allow speed changes in all animated MFMs @Johann150 | ||||
| - The theme color is now better validated. @Johann150 | ||||
|   Your own theme color may be unset if it was in an invalid format. | ||||
|   Admins should check their instance settings if in doubt. | ||||
|  | @ -30,20 +29,31 @@ You should also include the user name that made the change. | |||
|   Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - Client: fix settings page @tamaina | ||||
| - Client: fix profile tabs @futchitwo | ||||
| - Server: keep file order of note attachement @Johann150 | ||||
| - Server: fix caching @Johann150 | ||||
| - Server: await promises when following or unfollowing users @Johann150 | ||||
| - Client: fix abuse reports page to be able to show all reports @Johann150 | ||||
| - Federation: Add rel attribute to host-meta @mei23 | ||||
| - Client: fix profile picture height in mentions @tamaina | ||||
| - MFM: more animated functions support `speed` parameter @futchitwo | ||||
| - Federation: Fix quote renotes containing no text being federated correctly @Johann150 | ||||
| - Server: fix missing foreign key for reports leading to reports page being unusable @Johann150 | ||||
| - Server: fix internal in-memory caching @Johann150 | ||||
| - Server: use correct order of attachments on notes @Johann150 | ||||
| - Server: prevent crash when processing certain PNGs @syuilo | ||||
| - Server: Fix unable to generate video thumbnails @mei23 | ||||
| - Server: Fix `Cannot find module` issue @mei23 | ||||
| - Federation: Add rel attribute to host-meta @mei23 | ||||
| - Federation: add id for activitypub follows @Johann150 | ||||
| - Federation: ensure resolver does not fetch local resources via HTTP(S) @Johann150 | ||||
| - Federation: correctly render empty note text @Johann150 | ||||
| - Federation: Fix quote renotes containing no text being federated correctly @Johann150 | ||||
| - Federation: remove duplicate br tag/newline @Johann150 | ||||
| - Federation: add missing authorization checks @Johann150 | ||||
| - Client: fix profile picture height in mentions @tamaina | ||||
| - Client: fix abuse reports page to be able to show all reports @Johann150 | ||||
| - Client: fix settings page @tamaina | ||||
| - Client: fix profile tabs @futchitwo | ||||
| - Client: fix popout URL @futchitwo | ||||
| - Client: correctly handle MiAuth URLs with query string @sn0w | ||||
| - Client: ノート詳細ページの新しいノートを表示する機能の動作が正しくなるように修正する @xianonn | ||||
| - MFM: more animated functions support `speed` parameter @futchitwo | ||||
| - MFM: limit large MFM @Johann150 | ||||
| 
 | ||||
| ## 12.110.1 (2022/04/23) | ||||
| 
 | ||||
|  |  | |||
|  | @ -66,20 +66,29 @@ Be willing to comment on the good points and not just the things you want fixed | |||
| 	- Are there any omissions or gaps? | ||||
| 	- Does it check for anomalies? | ||||
| 
 | ||||
| ## Deploy | ||||
| The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. | ||||
| ``` | ||||
| /deploy sha=<commit hash> | ||||
| ``` | ||||
| An actual domain will be assigned so you can test the federation. | ||||
| 
 | ||||
| ## Merge | ||||
| For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase. | ||||
| However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor. | ||||
| 
 | ||||
| ## Release | ||||
| For now, basically only @syuilo has the authority to release Misskey. | ||||
| However, in case of emergency, a release can be made at the discretion of a contributor. | ||||
| 
 | ||||
| ### Release Instructions | ||||
| 1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) | ||||
| 2. follow the `master` branch to the `develop` branch. | ||||
| 3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) | ||||
|   - The target branch must be `master` | ||||
|   - The tag name must be the version | ||||
| 1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) | ||||
| 2. Create a release PR. | ||||
| 	- Into `master` from `develop` branch. | ||||
| 	- The title must be in the format `Release: x.y.z`. | ||||
| 		- `x.y.z` is the new version you are trying to release. | ||||
| 3. Deploy and perform a simple QA check. Also verify that the tests passed. | ||||
| 4. Merge it. | ||||
| 5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) | ||||
| 	- The target branch must be `master` | ||||
| 	- The tag name must be the version | ||||
| 
 | ||||
| ## Localization (l10n) | ||||
| Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. | ||||
|  |  | |||
|  | @ -5,6 +5,6 @@ | |||
| 		"loader=./test/loader.js" | ||||
| 	], | ||||
| 	"slow": 1000, | ||||
| 	"timeout": 3000, | ||||
| 	"timeout": 10000, | ||||
| 	"exit": true | ||||
| } | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ | |||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"style-loader": "3.3.1", | ||||
| 		"summaly": "2.5.0", | ||||
| 		"summaly": "2.5.1", | ||||
| 		"syslog-pro": "1.0.0", | ||||
| 		"systeminformation": "5.11.15", | ||||
| 		"tinycolor2": "1.4.2", | ||||
|  |  | |||
|  | @ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js'; | |||
| import { Webhook } from '@/models/entities/webhook.js'; | ||||
| import { envOption } from '../env.js'; | ||||
| import { dbLogger } from './logger.js'; | ||||
| import { redisClient } from './redis.js'; | ||||
| 
 | ||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); | ||||
| 
 | ||||
|  | @ -207,7 +208,15 @@ export const db = new DataSource({ | |||
| 	migrations: ['../../migration/*.js'], | ||||
| }); | ||||
| 
 | ||||
| export async function initDb() { | ||||
| export async function initDb(force = false) { | ||||
| 	if (force) { | ||||
| 		if (db.isInitialized) { | ||||
| 			await db.destroy(); | ||||
| 		} | ||||
| 		await db.initialize(); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	if (db.isInitialized) { | ||||
| 		// nop
 | ||||
| 	} else { | ||||
|  | @ -217,6 +226,7 @@ export async function initDb() { | |||
| 
 | ||||
| export async function resetDb() { | ||||
| 	const reset = async () => { | ||||
| 		await redisClient.FLUSHDB(); | ||||
| 		const tables = await db.query(`SELECT relname AS "table"
 | ||||
| 		FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) | ||||
| 		WHERE nspname NOT IN ('pg_catalog', 'information_schema') | ||||
|  |  | |||
|  | @ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ | |||
| 
 | ||||
| 	getPublicProperties(file: DriveFile): DriveFile['properties'] { | ||||
| 		if (file.properties.orientation != null) { | ||||
| 			const properties = structuredClone(file.properties); | ||||
| 			// TODO
 | ||||
| 			//const properties = structuredClone(file.properties);
 | ||||
| 			const properties = JSON.parse(JSON.stringify(file.properties)); | ||||
| 			if (file.properties.orientation >= 5) { | ||||
| 				[properties.width, properties.height] = [properties.height, properties.width]; | ||||
| 			} | ||||
|  |  | |||
|  | @ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/ | |||
| import { UserPublickey } from '@/models/entities/user-publickey.js'; | ||||
| import { MessagingMessage } from '@/models/entities/messaging-message.js'; | ||||
| import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; | ||||
| import { IObject, getApId } from './type.js'; | ||||
| import { resolvePerson } from './models/person.js'; | ||||
| import { Cache } from '@/misc/cache.js'; | ||||
| import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; | ||||
| import { IObject, getApId } from './type.js'; | ||||
| import { resolvePerson } from './models/person.js'; | ||||
| 
 | ||||
| const publicKeyCache = new Cache<UserPublickey | null>(Infinity); | ||||
| const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); | ||||
| 
 | ||||
| export type UriParseResult = { | ||||
| 	/** wether the URI was generated by us */ | ||||
| 	local: true; | ||||
| 	/** id in DB */ | ||||
| 	id: string; | ||||
| 	/** hint of type, e.g. "notes", "users" */ | ||||
| 	type: string; | ||||
| 	/** any remaining text after type and id, not including the slash after id. undefined if empty */ | ||||
| 	rest?: string; | ||||
| } | { | ||||
| 	/** wether the URI was generated by us */ | ||||
| 	local: false; | ||||
| 	/** uri in DB */ | ||||
| 	uri: string; | ||||
| }; | ||||
| 
 | ||||
| export function parseUri(value: string | IObject): UriParseResult { | ||||
| 	const uri = getApId(value); | ||||
| 
 | ||||
| 	// the host part of a URL is case insensitive, so use the 'i' flag.
 | ||||
| 	const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); | ||||
| 	const matchLocal = uri.match(localRegex); | ||||
| 
 | ||||
| 	if (matchLocal) { | ||||
| 		return { | ||||
| 			local: true, | ||||
| 			type: matchLocal[1], | ||||
| 			id: matchLocal[2], | ||||
| 			rest: matchLocal[3], | ||||
| 		}; | ||||
| 	} else { | ||||
| 		return { | ||||
| 			local: false, | ||||
| 			uri, | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export default class DbResolver { | ||||
| 	constructor() { | ||||
| 	} | ||||
|  | @ -21,60 +59,54 @@ export default class DbResolver { | |||
| 	 * AP Note => Misskey Note in DB | ||||
| 	 */ | ||||
| 	public async getNoteFromApId(value: string | IObject): Promise<Note | null> { | ||||
| 		const parsed = this.parseUri(value); | ||||
| 		const parsed = parseUri(value); | ||||
| 
 | ||||
| 		if (parsed.local) { | ||||
| 			if (parsed.type !== 'notes') return null; | ||||
| 
 | ||||
| 		if (parsed.id) { | ||||
| 			return await Notes.findOneBy({ | ||||
| 				id: parsed.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (parsed.uri) { | ||||
| 		} else { | ||||
| 			return await Notes.findOneBy({ | ||||
| 				uri: parsed.uri, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> { | ||||
| 		const parsed = this.parseUri(value); | ||||
| 		const parsed = parseUri(value); | ||||
| 
 | ||||
| 		if (parsed.local) { | ||||
| 			if (parsed.type !== 'notes') return null; | ||||
| 
 | ||||
| 		if (parsed.id) { | ||||
| 			return await MessagingMessages.findOneBy({ | ||||
| 				id: parsed.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (parsed.uri) { | ||||
| 		} else { | ||||
| 			return await MessagingMessages.findOneBy({ | ||||
| 				uri: parsed.uri, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * AP Person => Misskey User in DB | ||||
| 	 */ | ||||
| 	public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> { | ||||
| 		const parsed = this.parseUri(value); | ||||
| 		const parsed = parseUri(value); | ||||
| 
 | ||||
| 		if (parsed.local) { | ||||
| 			if (parsed.type !== 'users') return null; | ||||
| 
 | ||||
| 		if (parsed.id) { | ||||
| 			return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ | ||||
| 				id: parsed.id, | ||||
| 			}).then(x => x ?? undefined)) ?? null; | ||||
| 		} | ||||
| 
 | ||||
| 		if (parsed.uri) { | ||||
| 		} else { | ||||
| 			return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ | ||||
| 				uri: parsed.uri, | ||||
| 			})); | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|  | @ -120,31 +152,4 @@ export default class DbResolver { | |||
| 			key, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	public parseUri(value: string | IObject): UriParseResult { | ||||
| 		const uri = getApId(value); | ||||
| 
 | ||||
| 		const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)'); | ||||
| 		const matchLocal = uri.match(localRegex); | ||||
| 
 | ||||
| 		if (matchLocal) { | ||||
| 			return { | ||||
| 				type: matchLocal[1], | ||||
| 				id: matchLocal[2], | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				uri, | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type UriParseResult = { | ||||
| 	/** id in DB (local object only) */ | ||||
| 	id?: string; | ||||
| 	/** uri in DB (remote object only) */ | ||||
| 	uri?: string; | ||||
| 	/** hint of type (local object only, ex: notes, users) */ | ||||
| 	type?: string | ||||
| }; | ||||
|  |  | |||
|  | @ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js'; | |||
| import { toHtml } from '../../../mfm/to-html.js'; | ||||
| 
 | ||||
| export default function(note: Note) { | ||||
| 	let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; | ||||
| 	if (html == null) html = '<p>.</p>'; | ||||
| 
 | ||||
| 	return html; | ||||
| 	if (!note.text) return ''; | ||||
| 	return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,20 @@ | |||
| import config from '@/config/index.js'; | ||||
| import { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; | ||||
| import { Blocking } from '@/models/entities/blocking.js'; | ||||
| 
 | ||||
| export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ | ||||
| 	type: 'Block', | ||||
| 	actor: `${config.url}/users/${blocker.id}`, | ||||
| 	object: blockee.uri, | ||||
| }); | ||||
| /** | ||||
|  * Renders a block into its ActivityPub representation. | ||||
|  * | ||||
|  * @param block The block to be rendered. The blockee relation must be loaded. | ||||
|  */ | ||||
| export function renderBlock(block: Blocking) { | ||||
| 	if (block.blockee?.url == null) { | ||||
| 		throw new Error('renderBlock: missing blockee uri'); | ||||
| 	} | ||||
| 
 | ||||
| 	return { | ||||
| 		type: 'Block', | ||||
| 		id: `${config.url}/blocks/${block.id}`, | ||||
| 		actor: `${config.url}/users/${block.blockerId}`, | ||||
| 		object: block.blockee.uri, | ||||
| 	}; | ||||
| } | ||||
|  |  | |||
|  | @ -4,12 +4,11 @@ import { Users } from '@/models/index.js'; | |||
| 
 | ||||
| export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { | ||||
| 	const follow = { | ||||
| 		id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, | ||||
| 		type: 'Follow', | ||||
| 		actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, | ||||
| 		object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, | ||||
| 	} as any; | ||||
| 
 | ||||
| 	if (requestId) follow.id = requestId; | ||||
| 
 | ||||
| 	return follow; | ||||
| }; | ||||
|  |  | |||
|  | @ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false | |||
| 
 | ||||
| 	const files = await getPromisedFiles(note.fileIds); | ||||
| 
 | ||||
| 	const text = note.text; | ||||
| 	// text should never be undefined
 | ||||
| 	const text = note.text ?? null; | ||||
| 	let poll: Poll | null = null; | ||||
| 
 | ||||
| 	if (note.hasPoll) { | ||||
| 		poll = await Polls.findOneBy({ noteId: note.id }); | ||||
| 	} | ||||
| 
 | ||||
| 	let apText = text; | ||||
| 	if (apText == null) apText = ''; | ||||
| 	let apText = text ?? ''; | ||||
| 
 | ||||
| 	if (quote) { | ||||
| 		apText += `\n\nRE: ${quote}`; | ||||
|  |  | |||
|  | @ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js'; | |||
| import { ILocalUser } from '@/models/entities/user.js'; | ||||
| import { getInstanceActor } from '@/services/instance-actor.js'; | ||||
| import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||
| import { extractDbHost } from '@/misc/convert-host.js'; | ||||
| import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; | ||||
| import { signedGet } from './request.js'; | ||||
| import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; | ||||
| import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js'; | ||||
| import { parseUri } from './db-resolver.js'; | ||||
| import renderNote from '@/remote/activitypub/renderer/note.js'; | ||||
| import { renderLike } from '@/remote/activitypub/renderer/like.js'; | ||||
| import { renderPerson } from '@/remote/activitypub/renderer/person.js'; | ||||
| import renderQuestion from '@/remote/activitypub/renderer/question.js'; | ||||
| import renderCreate from '@/remote/activitypub/renderer/create.js'; | ||||
| import { renderActivity } from '@/remote/activitypub/renderer/index.js'; | ||||
| import renderFollow from '@/remote/activitypub/renderer/follow.js'; | ||||
| 
 | ||||
| export default class Resolver { | ||||
| 	private history: Set<string>; | ||||
|  | @ -40,14 +49,25 @@ export default class Resolver { | |||
| 			return value; | ||||
| 		} | ||||
| 
 | ||||
| 		if (value.includes('#')) { | ||||
| 			// URLs with fragment parts cannot be resolved correctly because
 | ||||
| 			// the fragment part does not get transmitted over HTTP(S).
 | ||||
| 			// Avoid strange behaviour by not trying to resolve these at all.
 | ||||
| 			throw new Error(`cannot resolve URL with fragment: ${value}`); | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.history.has(value)) { | ||||
| 			throw new Error('cannot resolve already resolved one'); | ||||
| 		} | ||||
| 
 | ||||
| 		this.history.add(value); | ||||
| 
 | ||||
| 		const meta = await fetchMeta(); | ||||
| 		const host = extractDbHost(value); | ||||
| 		if (isSelfHost(host)) { | ||||
| 			return await this.resolveLocal(value); | ||||
| 		} | ||||
| 
 | ||||
| 		const meta = await fetchMeta(); | ||||
| 		if (meta.blockedHosts.includes(host)) { | ||||
| 			throw new Error('Instance is blocked'); | ||||
| 		} | ||||
|  | @ -70,4 +90,44 @@ export default class Resolver { | |||
| 
 | ||||
| 		return object; | ||||
| 	} | ||||
| 
 | ||||
| 	private resolveLocal(url: string): Promise<IObject> { | ||||
| 		const parsed = parseUri(url); | ||||
| 		if (!parsed.local) throw new Error('resolveLocal: not local'); | ||||
| 
 | ||||
| 		switch (parsed.type) { | ||||
| 			case 'notes': | ||||
| 				return Notes.findOneByOrFail({ id: parsed.id }) | ||||
| 				.then(note => { | ||||
| 					if (parsed.rest === 'activity') { | ||||
| 						// this refers to the create activity and not the note itself
 | ||||
| 						return renderActivity(renderCreate(renderNote(note))); | ||||
| 					} else { | ||||
| 						return renderNote(note); | ||||
| 					} | ||||
| 				}); | ||||
| 			case 'users': | ||||
| 				return Users.findOneByOrFail({ id: parsed.id }) | ||||
| 				.then(user => renderPerson(user as ILocalUser)); | ||||
| 			case 'questions': | ||||
| 				// Polls are indexed by the note they are attached to.
 | ||||
| 				return Promise.all([ | ||||
| 					Notes.findOneByOrFail({ id: parsed.id }), | ||||
| 					Polls.findOneByOrFail({ noteId: parsed.id }), | ||||
| 				]) | ||||
| 				.then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll)); | ||||
| 			case 'likes': | ||||
| 				return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null }))); | ||||
| 			case 'follows': | ||||
| 				// rest should be <followee id>
 | ||||
| 				if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); | ||||
| 
 | ||||
| 				return Promise.all( | ||||
| 					[parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id })) | ||||
| 				) | ||||
| 				.then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url))); | ||||
| 			default: | ||||
| 				throw new Error(`resolveLocal: type ${type} unhandled`); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js'; | |||
| import { isSelfHost } from '@/misc/convert-host.js'; | ||||
| import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; | ||||
| import { ILocalUser, User } from '@/models/entities/user.js'; | ||||
| import { In, IsNull } from 'typeorm'; | ||||
| import { In, IsNull, Not } from 'typeorm'; | ||||
| import { renderLike } from '@/remote/activitypub/renderer/like.js'; | ||||
| import { getUserKeypair } from '@/misc/keypair-store.js'; | ||||
| import renderFollow from '@/remote/activitypub/renderer/follow.js'; | ||||
| 
 | ||||
| // Init router
 | ||||
| const router = new Router(); | ||||
|  | @ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => { | |||
| 	setResponseType(ctx); | ||||
| }); | ||||
| 
 | ||||
| // follow
 | ||||
| router.get('/follows/:follower/:followee', async ctx => { | ||||
| 	// This may be used before the follow is completed, so we do not
 | ||||
| 	// check if the following exists.
 | ||||
| 
 | ||||
| 	const [follower, followee] = await Promise.all([ | ||||
| 		Users.findOneBy({ | ||||
| 			id: ctx.params.follower, | ||||
| 			host: IsNull(), | ||||
| 		}), | ||||
| 		Users.findOneBy({ | ||||
| 			id: ctx.params.followee, | ||||
| 			host: Not(IsNull()), | ||||
| 		}), | ||||
| 	]); | ||||
| 
 | ||||
| 	if (follower == null || followee == null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.body = renderActivity(renderFollow(follower, followee)); | ||||
| 	ctx.set('Cache-Control', 'public, max-age=180'); | ||||
| 	setResponseType(ctx); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Signins, UserProfiles, Users } from '@/models/index.js'; | ||||
| import define from '../../define.js'; | ||||
| import { Users } from '@/models/index.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['admin'], | ||||
|  | @ -23,9 +23,12 @@ export const paramDef = { | |||
| 
 | ||||
| // eslint-disable-next-line import/no-default-export
 | ||||
| export default define(meta, paramDef, async (ps, me) => { | ||||
| 	const user = await Users.findOneBy({ id: ps.userId }); | ||||
| 	const [user, profile] = await Promise.all([ | ||||
| 		Users.findOneBy({ id: ps.userId }), | ||||
| 		UserProfiles.findOneBy({ userId: ps.userId }) | ||||
| 	]); | ||||
| 
 | ||||
| 	if (user == null) { | ||||
| 	if (user == null || profile == null) { | ||||
| 		throw new Error('user not found'); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => { | |||
| 		throw new Error('cannot show info of admin'); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!_me.isAdmin) { | ||||
| 		return { | ||||
| 			isModerator: user.isModerator, | ||||
| 			isSilenced: user.isSilenced, | ||||
| 			isSuspended: user.isSuspended, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken']; | ||||
| 	Object.keys(profile.integrations).forEach(integration => { | ||||
| 		maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>'); | ||||
| 	}); | ||||
| 
 | ||||
| 	const signins = await Signins.findBy({ userId: user.id }); | ||||
| 
 | ||||
| 	return { | ||||
| 		...user, | ||||
| 		token: user.token != null ? '<MASKED>' : user.token, | ||||
| 		email: profile.email, | ||||
| 		emailVerified: profile.emailVerified, | ||||
| 		autoAcceptFollowed: profile.autoAcceptFollowed, | ||||
| 		noCrawle: profile.noCrawle, | ||||
| 		alwaysMarkNsfw: profile.alwaysMarkNsfw, | ||||
| 		carefulBot: profile.carefulBot, | ||||
| 		injectFeaturedNote: profile.injectFeaturedNote, | ||||
| 		receiveAnnouncementEmail: profile.receiveAnnouncementEmail, | ||||
| 		integrations: profile.integrations, | ||||
| 		mutedWords: profile.mutedWords, | ||||
| 		mutedInstances: profile.mutedInstances, | ||||
| 		mutingNotificationTypes: profile.mutingNotificationTypes, | ||||
| 		isModerator: user.isModerator, | ||||
| 		isSilenced: user.isSilenced, | ||||
| 		isSuspended: user.isSuspended, | ||||
| 		signins, | ||||
| 	}; | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import define from '../../define.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
| import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; | ||||
| import { awaitAll } from '@/prelude/await-all.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['users'], | ||||
|  | @ -31,109 +32,72 @@ export default define(meta, paramDef, async (ps, me) => { | |||
| 		throw new ApiError(meta.errors.noSuchUser); | ||||
| 	} | ||||
| 
 | ||||
| 	const [ | ||||
| 		notesCount, | ||||
| 		repliesCount, | ||||
| 		renotesCount, | ||||
| 		repliedCount, | ||||
| 		renotedCount, | ||||
| 		pollVotesCount, | ||||
| 		pollVotedCount, | ||||
| 		localFollowingCount, | ||||
| 		remoteFollowingCount, | ||||
| 		localFollowersCount, | ||||
| 		remoteFollowersCount, | ||||
| 		sentReactionsCount, | ||||
| 		receivedReactionsCount, | ||||
| 		noteFavoritesCount, | ||||
| 		pageLikesCount, | ||||
| 		pageLikedCount, | ||||
| 		driveFilesCount, | ||||
| 		driveUsage, | ||||
| 	] = await Promise.all([ | ||||
| 		Notes.createQueryBuilder('note') | ||||
| 	const result = await awaitAll({ | ||||
| 		notesCount: Notes.createQueryBuilder('note') | ||||
| 			.where('note.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		Notes.createQueryBuilder('note') | ||||
| 		repliesCount: Notes.createQueryBuilder('note') | ||||
| 			.where('note.userId = :userId', { userId: user.id }) | ||||
| 			.andWhere('note.replyId IS NOT NULL') | ||||
| 			.getCount(), | ||||
| 		Notes.createQueryBuilder('note') | ||||
| 		renotesCount: Notes.createQueryBuilder('note') | ||||
| 			.where('note.userId = :userId', { userId: user.id }) | ||||
| 			.andWhere('note.renoteId IS NOT NULL') | ||||
| 			.getCount(), | ||||
| 		Notes.createQueryBuilder('note') | ||||
| 		repliedCount: Notes.createQueryBuilder('note') | ||||
| 			.where('note.replyUserId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		Notes.createQueryBuilder('note') | ||||
| 		renotedCount: Notes.createQueryBuilder('note') | ||||
| 			.where('note.renoteUserId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		PollVotes.createQueryBuilder('vote') | ||||
| 		pollVotesCount: PollVotes.createQueryBuilder('vote') | ||||
| 			.where('vote.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		PollVotes.createQueryBuilder('vote') | ||||
| 		pollVotedCount: PollVotes.createQueryBuilder('vote') | ||||
| 			.innerJoin('vote.note', 'note') | ||||
| 			.where('note.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		Followings.createQueryBuilder('following') | ||||
| 		localFollowingCount: Followings.createQueryBuilder('following') | ||||
| 			.where('following.followerId = :userId', { userId: user.id }) | ||||
| 			.andWhere('following.followeeHost IS NULL') | ||||
| 			.getCount(), | ||||
| 		Followings.createQueryBuilder('following') | ||||
| 		remoteFollowingCount: Followings.createQueryBuilder('following') | ||||
| 			.where('following.followerId = :userId', { userId: user.id }) | ||||
| 			.andWhere('following.followeeHost IS NOT NULL') | ||||
| 			.getCount(), | ||||
| 		Followings.createQueryBuilder('following') | ||||
| 		localFollowersCount: Followings.createQueryBuilder('following') | ||||
| 			.where('following.followeeId = :userId', { userId: user.id }) | ||||
| 			.andWhere('following.followerHost IS NULL') | ||||
| 			.getCount(), | ||||
| 		Followings.createQueryBuilder('following') | ||||
| 		remoteFollowersCount: Followings.createQueryBuilder('following') | ||||
| 			.where('following.followeeId = :userId', { userId: user.id }) | ||||
| 			.andWhere('following.followerHost IS NOT NULL') | ||||
| 			.getCount(), | ||||
| 		NoteReactions.createQueryBuilder('reaction') | ||||
| 		sentReactionsCount: NoteReactions.createQueryBuilder('reaction') | ||||
| 			.where('reaction.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		NoteReactions.createQueryBuilder('reaction') | ||||
| 		receivedReactionsCount: NoteReactions.createQueryBuilder('reaction') | ||||
| 			.innerJoin('reaction.note', 'note') | ||||
| 			.where('note.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		NoteFavorites.createQueryBuilder('favorite') | ||||
| 		noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') | ||||
| 			.where('favorite.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		PageLikes.createQueryBuilder('like') | ||||
| 		pageLikesCount: PageLikes.createQueryBuilder('like') | ||||
| 			.where('like.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		PageLikes.createQueryBuilder('like') | ||||
| 		pageLikedCount: PageLikes.createQueryBuilder('like') | ||||
| 			.innerJoin('like.page', 'page') | ||||
| 			.where('page.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		DriveFiles.createQueryBuilder('file') | ||||
| 		driveFilesCount: DriveFiles.createQueryBuilder('file') | ||||
| 			.where('file.userId = :userId', { userId: user.id }) | ||||
| 			.getCount(), | ||||
| 		DriveFiles.calcDriveUsageOf(user), | ||||
| 	]); | ||||
| 		driveUsage: DriveFiles.calcDriveUsageOf(user), | ||||
| 	}); | ||||
| 
 | ||||
| 	return { | ||||
| 		notesCount, | ||||
| 		repliesCount, | ||||
| 		renotesCount, | ||||
| 		repliedCount, | ||||
| 		renotedCount, | ||||
| 		pollVotesCount, | ||||
| 		pollVotedCount, | ||||
| 		localFollowingCount, | ||||
| 		remoteFollowingCount, | ||||
| 		localFollowersCount, | ||||
| 		remoteFollowersCount, | ||||
| 		followingCount: localFollowingCount + remoteFollowingCount, | ||||
| 		followersCount: localFollowersCount + remoteFollowersCount, | ||||
| 		sentReactionsCount, | ||||
| 		receivedReactionsCount, | ||||
| 		noteFavoritesCount, | ||||
| 		pageLikesCount, | ||||
| 		pageLikedCount, | ||||
| 		driveFilesCount, | ||||
| 		driveUsage, | ||||
| 	}; | ||||
| 	result.followingCount = result.localFollowingCount + result.remoteFollowingCount; | ||||
| 	result.followersCount = result.localFollowersCount + result.remoteFollowersCount; | ||||
| 
 | ||||
| 	return result; | ||||
| }); | ||||
|  |  | |||
|  | @ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; | |||
| import manifest from './manifest.json' assert { type: 'json' }; | ||||
| 
 | ||||
| export const manifestHandler = async (ctx: Koa.Context) => { | ||||
| 	const res = structuredClone(manifest); | ||||
| 	// TODO
 | ||||
| 	//const res = structuredClone(manifest);
 | ||||
| 	const res = JSON.parse(JSON.stringify(manifest)); | ||||
| 
 | ||||
| 	const instance = await fetchMeta(true); | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js'; | |||
| import { renderActivity } from '@/remote/activitypub/renderer/index.js'; | ||||
| import renderFollow from '@/remote/activitypub/renderer/follow.js'; | ||||
| import renderUndo from '@/remote/activitypub/renderer/undo.js'; | ||||
| import renderBlock from '@/remote/activitypub/renderer/block.js'; | ||||
| import { renderBlock } from '@/remote/activitypub/renderer/block.js'; | ||||
| import { deliver } from '@/queue/index.js'; | ||||
| import renderReject from '@/remote/activitypub/renderer/reject.js'; | ||||
| import { Blocking } from '@/models/entities/blocking.js'; | ||||
| import { User } from '@/models/entities/user.js'; | ||||
| import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; | ||||
| import { perUserFollowingChart } from '@/services/chart/index.js'; | ||||
|  | @ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) { | |||
| 		removeFromList(blockee, blocker), | ||||
| 	]); | ||||
| 
 | ||||
| 	await Blockings.insert({ | ||||
| 	const blocking = { | ||||
| 		id: genId(), | ||||
| 		createdAt: new Date(), | ||||
| 		blocker, | ||||
| 		blockerId: blocker.id, | ||||
| 		blockee, | ||||
| 		blockeeId: blockee.id, | ||||
| 	}); | ||||
| 	} as Blocking; | ||||
| 
 | ||||
| 	await Blockings.insert(blocking); | ||||
| 
 | ||||
| 	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { | ||||
| 		const content = renderActivity(renderBlock(blocker, blockee)); | ||||
| 		const content = renderActivity(renderBlock(blocking)); | ||||
| 		deliver(blocker, content, blockee.inbox); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { renderActivity } from '@/remote/activitypub/renderer/index.js'; | ||||
| import renderBlock from '@/remote/activitypub/renderer/block.js'; | ||||
| import { renderBlock } from '@/remote/activitypub/renderer/block.js'; | ||||
| import renderUndo from '@/remote/activitypub/renderer/undo.js'; | ||||
| import { deliver } from '@/queue/index.js'; | ||||
| import Logger from '../logger.js'; | ||||
|  | @ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) { | |||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// Since we already have the blocker and blockee, we do not need to fetch
 | ||||
| 	// them in the query above and can just manually insert them here.
 | ||||
| 	blocking.blocker = blocker; | ||||
| 	blocking.blockee = blockee; | ||||
| 
 | ||||
| 	Blockings.delete(blocking.id); | ||||
| 
 | ||||
| 	// deliver if remote bloking
 | ||||
| 	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { | ||||
| 		const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); | ||||
| 		const content = renderActivity(renderUndo(renderBlock(blocking), blocker)); | ||||
| 		deliver(blocker, content, blockee.inbox); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll | |||
| import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js'; | ||||
| import { entity as ApRequestChart } from './charts/entities/ap-request.js'; | ||||
| 
 | ||||
| import { entity as TestChart } from './charts/entities/test.js'; | ||||
| import { entity as TestGroupedChart } from './charts/entities/test-grouped.js'; | ||||
| import { entity as TestUniqueChart } from './charts/entities/test-unique.js'; | ||||
| import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js'; | ||||
| 
 | ||||
| export const entities = [ | ||||
| 	FederationChart.hour, FederationChart.day, | ||||
| 	NotesChart.hour, NotesChart.day, | ||||
|  | @ -24,4 +29,11 @@ export const entities = [ | |||
| 	PerUserFollowingChart.hour, PerUserFollowingChart.day, | ||||
| 	PerUserDriveChart.hour, PerUserDriveChart.day, | ||||
| 	ApRequestChart.hour, ApRequestChart.day, | ||||
| 
 | ||||
| 	...(process.env.NODE_ENV === 'test' ? [ | ||||
| 		TestChart.hour, TestChart.day, | ||||
| 		TestGroupedChart.hour, TestGroupedChart.day, | ||||
| 		TestUniqueChart.hour, TestUniqueChart.day, | ||||
| 		TestIntersectionChart.hour, TestIntersectionChart.day, | ||||
| 	] : []), | ||||
| ]; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { createSystemUser } from './create-system-user.js'; | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js'; | ||||
| import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; | ||||
| import renderUndo from '@/remote/activitypub/renderer/undo.js'; | ||||
|  | @ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js'; | |||
| import { genId } from '@/misc/gen-id.js'; | ||||
| import { Cache } from '@/misc/cache.js'; | ||||
| import { Relay } from '@/models/entities/relay.js'; | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { createSystemUser } from './create-system-user.js'; | ||||
| 
 | ||||
| const ACTOR_USERNAME = 'relay.actor' as const; | ||||
| 
 | ||||
|  | @ -88,7 +88,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act | |||
| 	})); | ||||
| 	if (relays.length === 0) return; | ||||
| 
 | ||||
| 	const copy = structuredClone(activity); | ||||
| 	// TODO
 | ||||
| 	//const copy = structuredClone(activity);
 | ||||
| 	const copy = JSON.parse(JSON.stringify(activity)); | ||||
| 	if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; | ||||
| 
 | ||||
| 	const signed = await attachLdSignature(copy, user); | ||||
|  |  | |||
|  | @ -2,11 +2,13 @@ process.env.NODE_ENV = 'test'; | |||
| 
 | ||||
| import * as assert from 'assert'; | ||||
| import rndstr from 'rndstr'; | ||||
| import { initDb } from '../src/db/postgre.js'; | ||||
| import { initTestDb } from './utils.js'; | ||||
| 
 | ||||
| describe('ActivityPub', () => { | ||||
| 	before(async () => { | ||||
| 		await initTestDb(); | ||||
| 		//await initTestDb();
 | ||||
| 		await initDb(); | ||||
| 	}); | ||||
| 
 | ||||
| 	describe('Parse minimum object', () => { | ||||
|  |  | |||
|  | @ -6,26 +6,17 @@ import TestChart from '../src/services/chart/charts/test.js'; | |||
| import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; | ||||
| import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; | ||||
| import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; | ||||
| import * as _TestChart from '../src/services/chart/charts/entities/test.js'; | ||||
| import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js'; | ||||
| import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js'; | ||||
| import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js'; | ||||
| import { async, initTestDb } from './utils.js'; | ||||
| import { initDb } from '../src/db/postgre.js'; | ||||
| 
 | ||||
| describe('Chart', () => { | ||||
| 	let testChart: TestChart; | ||||
| 	let testGroupedChart: TestGroupedChart; | ||||
| 	let testUniqueChart: TestUniqueChart; | ||||
| 	let testIntersectionChart: TestIntersectionChart; | ||||
| 	let clock: lolex.Clock; | ||||
| 	let clock: lolex.InstalledClock; | ||||
| 
 | ||||
| 	beforeEach(async(async () => { | ||||
| 		await initTestDb(false, [ | ||||
| 			_TestChart.entity.hour, _TestChart.entity.day, | ||||
| 			_TestGroupedChart.entity.hour, _TestGroupedChart.entity.day, | ||||
| 			_TestUniqueChart.entity.hour, _TestUniqueChart.entity.day, | ||||
| 			_TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day, | ||||
| 		]); | ||||
| 	beforeEach(async () => { | ||||
| 		await initDb(true); | ||||
| 
 | ||||
| 		testChart = new TestChart(); | ||||
| 		testGroupedChart = new TestGroupedChart(); | ||||
|  | @ -34,14 +25,15 @@ describe('Chart', () => { | |||
| 
 | ||||
| 		clock = lolex.install({ | ||||
| 			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), | ||||
| 			shouldClearNativeTimers: true, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(async(async () => { | ||||
| 	afterEach(() => { | ||||
| 		clock.uninstall(); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can updates', async(async () => { | ||||
| 	it('Can updates', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -63,9 +55,9 @@ describe('Chart', () => { | |||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can updates (dec)', async(async () => { | ||||
| 	it('Can updates (dec)', async () => { | ||||
| 		await testChart.decrement(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -87,9 +79,9 @@ describe('Chart', () => { | |||
| 				total: [-1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Empty chart', async(async () => { | ||||
| 	it('Empty chart', async () => { | ||||
| 		const chartHours = await testChart.getChart('hour', 3, null); | ||||
| 		const chartDays = await testChart.getChart('day', 3, null); | ||||
| 
 | ||||
|  | @ -108,9 +100,9 @@ describe('Chart', () => { | |||
| 				total: [0, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can updates at multiple times at same time', async(async () => { | ||||
| 	it('Can updates at multiple times at same time', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.increment(); | ||||
|  | @ -134,9 +126,9 @@ describe('Chart', () => { | |||
| 				total: [3, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('複数回saveされてもデータの更新は一度だけ', async(async () => { | ||||
| 	it('複数回saveされてもデータの更新は一度だけ', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 		await testChart.save(); | ||||
|  | @ -160,9 +152,9 @@ describe('Chart', () => { | |||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can updates at different times', async(async () => { | ||||
| 	it('Can updates at different times', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -189,11 +181,11 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
 | ||||
| 	/* | ||||
| 	it('Can updates at different times without save', async(async () => { | ||||
| 	it('Can updates at different times without save', async () => { | ||||
| 		await testChart.increment(); | ||||
| 
 | ||||
| 		clock.tick('01:00:00'); | ||||
|  | @ -219,10 +211,10 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0] | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 	*/ | ||||
| 
 | ||||
| 	it('Can padding', async(async () => { | ||||
| 	it('Can padding', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -249,10 +241,10 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	// 要求された範囲にログがひとつもない場合でもパディングできる
 | ||||
| 	it('Can padding from past range', async(async () => { | ||||
| 	it('Can padding from past range', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -276,11 +268,11 @@ describe('Chart', () => { | |||
| 				total: [1, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
 | ||||
| 	// Issue #3190
 | ||||
| 	it('Can padding from past range 2', async(async () => { | ||||
| 	it('Can padding from past range 2', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -307,9 +299,9 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can specify offset', async(async () => { | ||||
| 	it('Can specify offset', async () => { | ||||
| 		await testChart.increment(); | ||||
| 		await testChart.save(); | ||||
| 
 | ||||
|  | @ -336,9 +328,9 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	it('Can specify offset (floor time)', async(async () => { | ||||
| 	it('Can specify offset (floor time)', async () => { | ||||
| 		clock.tick('00:30:00'); | ||||
| 
 | ||||
| 		await testChart.increment(); | ||||
|  | @ -367,10 +359,10 @@ describe('Chart', () => { | |||
| 				total: [2, 0, 0], | ||||
| 			}, | ||||
| 		}); | ||||
| 	})); | ||||
| 	}); | ||||
| 
 | ||||
| 	describe('Grouped', () => { | ||||
| 		it('Can updates', async(async () => { | ||||
| 		it('Can updates', async () => { | ||||
| 			await testGroupedChart.increment('alice'); | ||||
| 			await testGroupedChart.save(); | ||||
| 
 | ||||
|  | @ -410,11 +402,11 @@ describe('Chart', () => { | |||
| 					total: [0, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	describe('Unique increment', () => { | ||||
| 		it('Can updates', async(async () => { | ||||
| 		it('Can updates', async () => { | ||||
| 			await testUniqueChart.uniqueIncrement('alice'); | ||||
| 			await testUniqueChart.uniqueIncrement('alice'); | ||||
| 			await testUniqueChart.uniqueIncrement('bob'); | ||||
|  | @ -430,10 +422,10 @@ describe('Chart', () => { | |||
| 			assert.deepStrictEqual(chartDays, { | ||||
| 				foo: [2, 0, 0], | ||||
| 			}); | ||||
| 		})); | ||||
| 		}); | ||||
| 
 | ||||
| 		describe('Intersection', () => { | ||||
| 			it('条件が満たされていない場合はカウントされない', async(async () => { | ||||
| 			it('条件が満たされていない場合はカウントされない', async () => { | ||||
| 				await testIntersectionChart.addA('alice'); | ||||
| 				await testIntersectionChart.addA('bob'); | ||||
| 				await testIntersectionChart.addB('carol'); | ||||
|  | @ -453,9 +445,9 @@ describe('Chart', () => { | |||
| 					b: [1, 0, 0], | ||||
| 					aAndB: [0, 0, 0], | ||||
| 				}); | ||||
| 			})); | ||||
| 			}); | ||||
| 
 | ||||
| 			it('条件が満たされている場合にカウントされる', async(async () => { | ||||
| 			it('条件が満たされている場合にカウントされる', async () => { | ||||
| 				await testIntersectionChart.addA('alice'); | ||||
| 				await testIntersectionChart.addA('bob'); | ||||
| 				await testIntersectionChart.addB('carol'); | ||||
|  | @ -476,12 +468,12 @@ describe('Chart', () => { | |||
| 					b: [2, 0, 0], | ||||
| 					aAndB: [1, 0, 0], | ||||
| 				}); | ||||
| 			})); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	describe('Resync', () => { | ||||
| 		it('Can resync', async(async () => { | ||||
| 		it('Can resync', async () => { | ||||
| 			testChart.total = 1; | ||||
| 
 | ||||
| 			await testChart.resync(); | ||||
|  | @ -504,9 +496,9 @@ describe('Chart', () => { | |||
| 					total: [1, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
| 		}); | ||||
| 
 | ||||
| 		it('Can resync (2)', async(async () => { | ||||
| 		it('Can resync (2)', async () => { | ||||
| 			await testChart.increment(); | ||||
| 			await testChart.save(); | ||||
| 
 | ||||
|  | @ -534,6 +526,6 @@ describe('Chart', () => { | |||
| 					total: [100, 0, 0], | ||||
| 				}, | ||||
| 			}); | ||||
| 		})); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  |  | |||
|  | @ -6520,16 +6520,17 @@ style-loader@3.3.1: | |||
|   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" | ||||
|   integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== | ||||
| 
 | ||||
| summaly@2.5.0: | ||||
|   version "2.5.0" | ||||
|   resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea" | ||||
|   integrity sha512-IzvO2s7yj/PUyH42qWjVjSPpIiPlgTRWGh33t4cIZKOqPQJ2INo7e83hXhHFr4hXTb3JRcIdCuM1ELjlrujiUQ== | ||||
| summaly@2.5.1: | ||||
|   version "2.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.1.tgz#742fe6631987f84ad2e95d2b0f7902ec57e0f6b3" | ||||
|   integrity sha512-WWvl7rLs3wm61Xc2JqgTbSuqtIOmGqKte+rkbnxe6ISy4089lQ+7F2ajooQNee6PWHl9kZ27SDd1ZMoL3/6R4A== | ||||
|   dependencies: | ||||
|     cheerio "0.22.0" | ||||
|     debug "4.3.3" | ||||
|     escape-regexp "0.0.1" | ||||
|     got "11.5.1" | ||||
|     html-entities "2.3.2" | ||||
|     iconv-lite "0.6.3" | ||||
|     jschardet "3.0.0" | ||||
|     koa "2.13.4" | ||||
|     private-ip "2.3.3" | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> | ||||
| 	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()"> | ||||
| 	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()"> | ||||
| 	<div ref="emojis" class="emojis"> | ||||
| 		<section class="result"> | ||||
| 			<div v-if="searchResultCustom.length > 0"> | ||||
|  |  | |||
|  | @ -39,8 +39,8 @@ export default defineComponent({ | |||
| 
 | ||||
| 	inject: { | ||||
| 		sideViewHook: { | ||||
| 			default: null | ||||
| 		} | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	provide() { | ||||
|  | @ -94,31 +94,31 @@ export default defineComponent({ | |||
| 			}, { | ||||
| 				icon: 'fas fa-expand-alt', | ||||
| 				text: this.$ts.showInPage, | ||||
| 				action: this.expand | ||||
| 				action: this.expand, | ||||
| 			}, this.sideViewHook ? { | ||||
| 				icon: 'fas fa-columns', | ||||
| 				text: this.$ts.openInSideView, | ||||
| 				action: () => { | ||||
| 					this.sideViewHook(this.path); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 				}, | ||||
| 			} : undefined, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.popout, | ||||
| 				action: this.popout | ||||
| 				action: this.popout, | ||||
| 			}, null, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.openInNewTab, | ||||
| 				action: () => { | ||||
| 					window.open(this.url, '_blank'); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 				}, | ||||
| 			}, { | ||||
| 				icon: 'fas fa-link', | ||||
| 				text: this.$ts.copyLink, | ||||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				} | ||||
| 				}, | ||||
| 			}]; | ||||
| 		}, | ||||
| 	}, | ||||
|  | @ -155,7 +155,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		onContextmenu(ev: MouseEvent) { | ||||
| 			os.contextMenu(this.contextmenu, ev); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -222,7 +222,7 @@ function react(viaKeyboard = false): void { | |||
| 	reactionPicker.show(reactButton.value, reaction => { | ||||
| 		os.api('notes/reactions/create', { | ||||
| 			noteId: appearNote.id, | ||||
| 			reaction: reaction | ||||
| 			reaction: reaction, | ||||
| 		}); | ||||
| 	}, () => { | ||||
| 		focus(); | ||||
|  | @ -233,7 +233,7 @@ function undoReact(note): void { | |||
| 	const oldReaction = note.myReaction; | ||||
| 	if (!oldReaction) return; | ||||
| 	os.api('notes/reactions/delete', { | ||||
| 		noteId: note.id | ||||
| 		noteId: note.id, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void { | |||
| 
 | ||||
| function menu(viaKeyboard = false): void { | ||||
| 	os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { | ||||
| 		viaKeyboard | ||||
| 		viaKeyboard, | ||||
| 	}).then(focus); | ||||
| } | ||||
| 
 | ||||
|  | @ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void { | |||
| 		danger: true, | ||||
| 		action: () => { | ||||
| 			os.api('notes/delete', { | ||||
| 				noteId: note.id | ||||
| 				noteId: note.id, | ||||
| 			}); | ||||
| 			isDeleted.value = true; | ||||
| 		} | ||||
| 		}, | ||||
| 	}], renoteTime.value, { | ||||
| 		viaKeyboard: viaKeyboard | ||||
| 		viaKeyboard: viaKeyboard, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -288,14 +288,14 @@ function blur() { | |||
| 
 | ||||
| os.api('notes/children', { | ||||
| 	noteId: appearNote.id, | ||||
| 	limit: 30 | ||||
| 	limit: 30, | ||||
| }).then(res => { | ||||
| 	replies.value = res; | ||||
| }); | ||||
| 
 | ||||
| if (appearNote.replyId) { | ||||
| 	os.api('notes/conversation', { | ||||
| 		noteId: appearNote.replyId | ||||
| 		noteId: appearNote.replyId, | ||||
| 	}).then(res => { | ||||
| 		conversation.value = res.reverse(); | ||||
| 	}); | ||||
|  |  | |||
|  | @ -210,7 +210,7 @@ function react(viaKeyboard = false): void { | |||
| 	reactionPicker.show(reactButton.value, reaction => { | ||||
| 		os.api('notes/reactions/create', { | ||||
| 			noteId: appearNote.id, | ||||
| 			reaction: reaction | ||||
| 			reaction: reaction, | ||||
| 		}); | ||||
| 	}, () => { | ||||
| 		focus(); | ||||
|  | @ -221,7 +221,7 @@ function undoReact(note): void { | |||
| 	const oldReaction = note.myReaction; | ||||
| 	if (!oldReaction) return; | ||||
| 	os.api('notes/reactions/delete', { | ||||
| 		noteId: note.id | ||||
| 		noteId: note.id, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void { | |||
| 
 | ||||
| function menu(viaKeyboard = false): void { | ||||
| 	os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, { | ||||
| 		viaKeyboard | ||||
| 		viaKeyboard, | ||||
| 	}).then(focus); | ||||
| } | ||||
| 
 | ||||
|  | @ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void { | |||
| 		danger: true, | ||||
| 		action: () => { | ||||
| 			os.api('notes/delete', { | ||||
| 				noteId: note.id | ||||
| 				noteId: note.id, | ||||
| 			}); | ||||
| 			isDeleted.value = true; | ||||
| 		} | ||||
| 		}, | ||||
| 	}], renoteTime.value, { | ||||
| 		viaKeyboard: viaKeyboard | ||||
| 		viaKeyboard: viaKeyboard, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -284,7 +284,7 @@ function focusAfter() { | |||
| 
 | ||||
| function readPromo() { | ||||
| 	os.api('promo/read', { | ||||
| 		noteId: appearNote.id | ||||
| 		noteId: appearNote.id, | ||||
| 	}); | ||||
| 	isDeleted.value = true; | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <XModalWindow ref="dialog" | ||||
| <XModalWindow | ||||
| 	ref="dialog" | ||||
| 	:width="400" | ||||
| 	:height="450" | ||||
| 	:with-ok-button="true" | ||||
|  | @ -28,18 +29,18 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, PropType } from 'vue'; | ||||
| import XModalWindow from '@/components/ui/modal-window.vue'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import MkSwitch from './form/switch.vue'; | ||||
| import MkInfo from './ui/info.vue'; | ||||
| import MkButton from './ui/button.vue'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import XModalWindow from '@/components/ui/modal-window.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XModalWindow, | ||||
| 		MkSwitch, | ||||
| 		MkInfo, | ||||
| 		MkButton | ||||
| 		MkButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -53,7 +54,7 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true, | ||||
| 		} | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['done', 'closed'], | ||||
|  | @ -93,7 +94,7 @@ export default defineComponent({ | |||
| 			for (const type in this.typesMap) { | ||||
| 				this.typesMap[type as typeof notificationTypes[number]] = true; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -16,7 +16,8 @@ | |||
| 			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i> | ||||
| 			<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i> | ||||
| 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> | ||||
| 			<XReactionIcon v-else-if="notification.type === 'reaction'" | ||||
| 			<XReactionIcon | ||||
| 				v-else-if="notification.type === 'reaction'" | ||||
| 				ref="reactionRef" | ||||
| 				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" | ||||
| 				:custom-emojis="notification.note.emojis" | ||||
|  | @ -74,10 +75,10 @@ | |||
| <script lang="ts"> | ||||
| import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||
| import XReactionIcon from './reaction-icon.vue'; | ||||
| import MkFollowButton from './follow-button.vue'; | ||||
| import XReactionTooltip from './reaction-tooltip.vue'; | ||||
| import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | @ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip'; | |||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XReactionIcon, MkFollowButton | ||||
| 		XReactionIcon, MkFollowButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -116,7 +117,7 @@ export default defineComponent({ | |||
| 				const readObserver = new IntersectionObserver((entries, observer) => { | ||||
| 					if (!entries.some(entry => entry.isIntersecting)) return; | ||||
| 					stream.send('readNotification', { | ||||
| 						id: props.notification.id | ||||
| 						id: props.notification.id, | ||||
| 					}); | ||||
| 					observer.disconnect(); | ||||
| 				}); | ||||
|  |  | |||
|  | @ -19,8 +19,7 @@ | |||
| <script lang="ts" setup> | ||||
| import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { Paging } from '@/components/ui/pagination.vue'; | ||||
| import MkPagination, { Paging } from '@/components/ui/pagination.vue'; | ||||
| import XNotification from '@/components/notification.vue'; | ||||
| import XList from '@/components/date-separated-list.vue'; | ||||
| import XNote from '@/components/note.vue'; | ||||
|  | @ -49,14 +48,14 @@ const onNotification = (notification) => { | |||
| 	const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); | ||||
| 	if (isMuted || document.visibilityState === 'visible') { | ||||
| 		stream.send('readNotification', { | ||||
| 			id: notification.id | ||||
| 			id: notification.id, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!isMuted) { | ||||
| 		pagingComponent.value.prepend({ | ||||
| 			...notification, | ||||
| 			isRead: document.visibilityState === 'visible' | ||||
| 			isRead: document.visibilityState === 'visible', | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ export default defineComponent({ | |||
| 	props: { | ||||
| 		value: { | ||||
| 			type: Number, | ||||
| 			required: true | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -26,7 +26,7 @@ export default defineComponent({ | |||
| 			isZero, | ||||
| 			number, | ||||
| 		}; | ||||
| 	} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <div ref="itemsEl" v-hotkey="keymap" | ||||
| <div | ||||
| 	ref="itemsEl" v-hotkey="keymap" | ||||
| 	class="rrevdjwt" | ||||
| 	:class="{ center: align === 'center', asDrawer }" | ||||
| 	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" | ||||
|  | @ -162,6 +163,15 @@ function focusDown() { | |||
| 			position: relative; | ||||
| 		} | ||||
| 
 | ||||
| 		&:not(:disabled):hover { | ||||
| 			color: var(--accent); | ||||
| 			text-decoration: none; | ||||
| 
 | ||||
| 			&:before { | ||||
| 				background: var(--accentedBg); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&.danger { | ||||
| 			color: #ff2a2a; | ||||
| 
 | ||||
|  | @ -191,15 +201,6 @@ function focusDown() { | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&:not(:disabled):hover { | ||||
| 			color: var(--accent); | ||||
| 			text-decoration: none; | ||||
| 
 | ||||
| 			&:before { | ||||
| 				background: var(--accentedBg); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&:not(:active):focus-visible { | ||||
| 			box-shadow: 0 0 0 2px var(--focus) inset; | ||||
| 		} | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { login } from '@/account'; | ||||
| import { appendQuery, query } from '@/scripts/url'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -82,7 +83,9 @@ export default defineComponent({ | |||
| 
 | ||||
| 			this.state = 'accepted'; | ||||
| 			if (this.callback) { | ||||
| 				location.href = `${this.callback}?session=${this.session}`; | ||||
| 				location.href = appendQuery(this.callback, query({ | ||||
| 					session: this.session | ||||
| 				})); | ||||
| 			} | ||||
| 		}, | ||||
| 		deny() { | ||||
|  |  | |||
|  | @ -54,6 +54,9 @@ | |||
| 				<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<MkObjectView v-if="info && $i.isAdmin" tall :value="info"> | ||||
| 			</MkObjectView> | ||||
| 
 | ||||
| 			<MkObjectView tall :value="user"> | ||||
| 			</MkObjectView> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -94,10 +94,10 @@ function onStats(connStats) { | |||
| 	inPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||
| 	outPolygonPoints = `${viewBoxX - (stats.length - 1)},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`; | ||||
| 
 | ||||
| 	inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0]; | ||||
| 	inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1]; | ||||
| 	outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0]; | ||||
| 	outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1]; | ||||
| 	inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0]; | ||||
| 	inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1]; | ||||
| 	outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0]; | ||||
| 	outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1]; | ||||
| 
 | ||||
| 	inRecent = connStats.net.rx; | ||||
| 	outRecent = connStats.net.tx; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue