Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
		
						commit
						71c230b7b7
					
				
					 18 changed files with 85 additions and 167 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/labeler.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/labeler.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -4,5 +4,9 @@ | |||
| '🖥️Client': | ||||
| - packages/client/**/* | ||||
| 
 | ||||
| '🧪Test': | ||||
| - cypress/**/* | ||||
| - packages/backend/test/**/* | ||||
| 
 | ||||
| '‼️ wrong locales': | ||||
| - any: ['locales/*.yml', '!locales/ja-JP.yml'] | ||||
|  |  | |||
							
								
								
									
										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: | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ You should also include the user name that made the change. | |||
|   Your own theme color may be unset if it was in an invalid format. | ||||
|   Admins should check their instance settings if in doubt. | ||||
| - Perform port diagnosis at startup only when Listen fails @mei23 | ||||
| - Rate limiting is now also usable for non-authenticated users. @Johann150 | ||||
| - Rate limiting is now also usable for non-authenticated users. @Johann150 @mei23 | ||||
|   Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address. | ||||
| 
 | ||||
| ### Bugfixes | ||||
|  | @ -43,6 +43,7 @@ You should also include the user name that made the change. | |||
| - 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 | ||||
| 
 | ||||
| ## 12.110.1 (2022/04/23) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,6 @@ | |||
| describe('Before setup instance', () => { | ||||
| 	beforeEach(() => { | ||||
| 		cy.window(win => { | ||||
| 			win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 		}); | ||||
| 		cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 		cy.get('@reset').its('status').should('equal', 204); | ||||
| 		cy.reload(true); | ||||
| 		cy.resetState(); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(() => { | ||||
|  | @ -35,18 +30,10 @@ describe('Before setup instance', () => { | |||
| 
 | ||||
| describe('After setup instance', () => { | ||||
| 	beforeEach(() => { | ||||
| 		cy.window(win => { | ||||
| 			win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 		}); | ||||
| 		cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 		cy.get('@reset').its('status').should('equal', 204); | ||||
| 		cy.reload(true); | ||||
| 		cy.resetState(); | ||||
| 
 | ||||
| 		// インスタンス初期セットアップ
 | ||||
| 		cy.request('POST', '/api/admin/accounts/create', { | ||||
| 			username: 'admin', | ||||
| 			password: 'pass', | ||||
| 		}).its('body').as('admin'); | ||||
| 		cy.registerUser('admin', 'pass', true); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(() => { | ||||
|  | @ -76,24 +63,13 @@ describe('After setup instance', () => { | |||
| 
 | ||||
| describe('After user signup', () => { | ||||
| 	beforeEach(() => { | ||||
| 		cy.window(win => { | ||||
| 			win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 		}); | ||||
| 		cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 		cy.get('@reset').its('status').should('equal', 204); | ||||
| 		cy.reload(true); | ||||
| 		cy.resetState(); | ||||
| 
 | ||||
| 		// インスタンス初期セットアップ
 | ||||
| 		cy.request('POST', '/api/admin/accounts/create', { | ||||
| 			username: 'admin', | ||||
| 			password: 'pass', | ||||
| 		}).its('body').as('admin'); | ||||
| 		cy.registerUser('admin', 'pass', true); | ||||
| 
 | ||||
| 		// ユーザー作成
 | ||||
| 		cy.request('POST', '/api/signup', { | ||||
| 			username: 'alice', | ||||
| 			password: 'alice1234', | ||||
| 		}).its('body').as('alice'); | ||||
| 		cy.registerUser('alice', 'alice1234'); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(() => { | ||||
|  | @ -138,34 +114,15 @@ describe('After user signup', () => { | |||
| 
 | ||||
| describe('After user singed in', () => { | ||||
| 	beforeEach(() => { | ||||
| 		cy.window(win => { | ||||
| 			win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 		}); | ||||
| 		cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 		cy.get('@reset').its('status').should('equal', 204); | ||||
| 		cy.reload(true); | ||||
| 		cy.resetState(); | ||||
| 
 | ||||
| 		// インスタンス初期セットアップ
 | ||||
| 		cy.request('POST', '/api/admin/accounts/create', { | ||||
| 			username: 'admin', | ||||
| 			password: 'pass', | ||||
| 		}).its('body').as('admin'); | ||||
| 		cy.registerUser('admin', 'pass', true); | ||||
| 
 | ||||
| 		// ユーザー作成
 | ||||
| 		cy.request('POST', '/api/signup', { | ||||
| 			username: 'alice', | ||||
| 			password: 'alice1234', | ||||
| 		}).its('body').as('alice'); | ||||
| 		cy.registerUser('alice', 'alice1234'); | ||||
| 
 | ||||
| 		cy.visit('/'); | ||||
| 
 | ||||
| 		cy.intercept('POST', '/api/signin').as('signin'); | ||||
| 
 | ||||
| 		cy.get('[data-cy-signin]').click(); | ||||
| 		cy.get('[data-cy-signin-username] input').type('alice'); | ||||
| 		cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); | ||||
| 
 | ||||
| 		cy.wait('@signin').as('signedIn'); | ||||
| 		cy.login('alice', 'alice1234'); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(() => { | ||||
|  |  | |||
|  | @ -1,34 +1,15 @@ | |||
| describe('After user signed in', () => { | ||||
| 	beforeEach(() => { | ||||
| 		cy.window(win => { | ||||
| 			win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 		}); | ||||
| 		cy.resetState(); | ||||
| 		cy.viewport('macbook-16'); | ||||
| 		cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 		cy.get('@reset').its('status').should('equal', 204); | ||||
| 		cy.reload(true); | ||||
| 
 | ||||
| 		// インスタンス初期セットアップ
 | ||||
| 		cy.request('POST', '/api/admin/accounts/create', { | ||||
| 			username: 'admin', | ||||
| 			password: 'pass', | ||||
| 		}).its('body').as('admin'); | ||||
| 		cy.registerUser('admin', 'pass', true); | ||||
| 
 | ||||
| 		// ユーザー作成
 | ||||
| 		cy.request('POST', '/api/signup', { | ||||
| 			username: 'alice', | ||||
| 			password: 'alice1234', | ||||
| 		}).its('body').as('alice'); | ||||
| 		cy.registerUser('alice', 'alice1234'); | ||||
| 
 | ||||
| 		cy.visit('/'); | ||||
| 
 | ||||
| 		cy.intercept('POST', '/api/signin').as('signin'); | ||||
| 
 | ||||
| 		cy.get('[data-cy-signin]').click(); | ||||
| 		cy.get('[data-cy-signin-username] input').type('alice'); | ||||
| 		cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); | ||||
| 
 | ||||
| 		cy.wait('@signin').as('signedIn'); | ||||
| 		cy.login('alice', 'alice1234'); | ||||
| 	}); | ||||
| 
 | ||||
| 	afterEach(() => { | ||||
|  |  | |||
|  | @ -23,3 +23,33 @@ | |||
| //
 | ||||
| // -- This will overwrite an existing command --
 | ||||
| // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
 | ||||
| 
 | ||||
| Cypress.Commands.add('resetState', () => { | ||||
| 	cy.window(win => { | ||||
| 		win.indexedDB.deleteDatabase('keyval-store'); | ||||
| 	}); | ||||
| 	cy.request('POST', '/api/reset-db').as('reset'); | ||||
| 	cy.get('@reset').its('status').should('equal', 204); | ||||
| 	cy.reload(true); | ||||
| }); | ||||
| 
 | ||||
| Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => { | ||||
| 	const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup'; | ||||
| 
 | ||||
| 	cy.request('POST', route, { | ||||
| 		username: username, | ||||
| 		password: password, | ||||
| 	}).its('body').as(username); | ||||
| }); | ||||
| 
 | ||||
| Cypress.Commands.add('login', (username, password) => { | ||||
| 	cy.visit('/'); | ||||
| 
 | ||||
| 	cy.intercept('POST', '/api/signin').as('signin'); | ||||
| 
 | ||||
| 	cy.get('[data-cy-signin]').click(); | ||||
| 	cy.get('[data-cy-signin-username] input').type(username); | ||||
| 	cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); | ||||
| 
 | ||||
| 	cy.wait('@signin').as('signedIn'); | ||||
| }); | ||||
|  |  | |||
|  | @ -6,6 +6,9 @@ const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; | |||
| const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; | ||||
| 
 | ||||
| export function fromHtml(html: string, hashtagNames?: string[]): string { | ||||
| 	// some AP servers like Pixelfed use br tags as well as newlines
 | ||||
| 	html = html.replace(/<br\s?\/?>\r?\n/gi, '\n'); | ||||
| 
 | ||||
| 	const dom = parse5.parseFragment(html); | ||||
| 
 | ||||
| 	let text = ''; | ||||
|  |  | |||
							
								
								
									
										9
									
								
								packages/backend/src/misc/get-ip-hash.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/backend/src/misc/get-ip-hash.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import IPCIDR from 'ip-cidr'; | ||||
| 
 | ||||
| export function getIpHash(ip: string) { | ||||
| 	// because a single person may control many IPv6 addresses,
 | ||||
| 	// only a /64 subnet prefix of any IP will be taken into account.
 | ||||
| 	// (this means for IPv4 the entire address is used)
 | ||||
| 	const prefix = IPCIDR.createAddress(ip).mask(64); | ||||
| 	return 'ip-' + BigInt('0b' + prefix).toString(36); | ||||
| } | ||||
|  | @ -305,11 +305,13 @@ export default function() { | |||
| 	systemQueue.add('resyncCharts', { | ||||
| 	}, { | ||||
| 		repeat: { cron: '0 0 * * *' }, | ||||
| 		removeOnComplete: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	systemQueue.add('cleanCharts', { | ||||
| 	}, { | ||||
| 		repeat: { cron: '0 0 * * *' }, | ||||
| 		removeOnComplete: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	systemQueue.add('checkExpiredMutings', { | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import endpoints, { IEndpointMeta } from './endpoints.js'; | |||
| import { ApiError } from './error.js'; | ||||
| import { apiLogger } from './logger.js'; | ||||
| import { AccessToken } from '@/models/entities/access-token.js'; | ||||
| import IPCIDR from 'ip-cidr'; | ||||
| import { getIpHash } from '@/misc/get-ip-hash.js'; | ||||
| 
 | ||||
| const accessDenied = { | ||||
| 	message: 'Access denied.', | ||||
|  | @ -33,18 +33,13 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi | |||
| 		throw new ApiError(accessDenied); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ep.meta.requireCredential && ep.meta.limit && !isModerator) { | ||||
| 	if (ep.meta.limit && !isModerator) { | ||||
| 		// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
 | ||||
| 		let limitActor: string; | ||||
| 		if (user) { | ||||
| 			limitActor = user.id; | ||||
| 		} else { | ||||
| 			// because a single person may control many IPv6 addresses,
 | ||||
| 			// only a /64 subnet prefix of any IP will be taken into account.
 | ||||
| 			// (this means for IPv4 the entire address is used)
 | ||||
| 			const ip = IPCIDR.createAddress(ctx.ip).mask(64); | ||||
| 
 | ||||
| 			limitActor = 'ip-' + parseInt(ip, 2).toString(36); | ||||
| 			limitActor = getIpHash(ctx!.ip); | ||||
| 		} | ||||
| 
 | ||||
| 		const limit = Object.assign({}, ep.meta.limit); | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import { verifyLogin, hash } from '../2fa.js'; | |||
| import { randomBytes } from 'node:crypto'; | ||||
| import { IsNull } from 'typeorm'; | ||||
| import { limiter } from '../limiter.js'; | ||||
| import { getIpHash } from '@/misc/get-ip-hash.js'; | ||||
| 
 | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	ctx.set('Access-Control-Allow-Origin', config.url); | ||||
|  | @ -27,7 +28,7 @@ export default async (ctx: Koa.Context) => { | |||
| 
 | ||||
| 	try { | ||||
| 		// not more than 1 attempt per second and not more than 10 attempts per hour
 | ||||
| 		await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, ctx.ip); | ||||
| 		await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(ctx.ip)); | ||||
| 	} catch (err) { | ||||
| 		ctx.status = 429; | ||||
| 		ctx.body = { | ||||
|  |  | |||
|  | @ -312,7 +312,8 @@ export default async (user: { id: User['id']; username: User['username']; host: | |||
| 		endedPollNotificationQueue.add({ | ||||
| 			noteId: note.id, | ||||
| 		}, { | ||||
| 			delay | ||||
| 			delay, | ||||
| 			removeOnComplete: true, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineExpose, ref } from 'vue'; | ||||
| import * as tinycolor from 'tinycolor2'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ | |||
| 			:key="ids[0]" | ||||
| 			class="column" | ||||
| 			:column="columns.find(c => c.id === ids[0])" | ||||
| 			 :is-stacked="false" | ||||
| 			:is-stacked="false" | ||||
| 			:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }" | ||||
| 			@parent-focus="moveFocus(ids[0], $event)" | ||||
| 		/> | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -1,71 +0,0 @@ | |||
| /** | ||||
|  * webpack configuration | ||||
|  */ | ||||
| 
 | ||||
| const fs = require('fs'); | ||||
| const webpack = require('webpack'); | ||||
| 
 | ||||
| class WebpackOnBuildPlugin { | ||||
| 	constructor(callback) { | ||||
| 		this.callback = callback; | ||||
| 	} | ||||
| 
 | ||||
| 	apply(compiler) { | ||||
| 		compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const isProduction = process.env.NODE_ENV === 'production'; | ||||
| 
 | ||||
| const locales = require('../../locales'); | ||||
| const meta = require('../../package.json'); | ||||
| 
 | ||||
| module.exports = { | ||||
| 	target: 'webworker', | ||||
| 	entry: { | ||||
| 		['sw-lib']: './src/lib.ts' | ||||
| 	}, | ||||
| 	module: { | ||||
| 		rules: [{ | ||||
| 			test: /\.ts$/, | ||||
| 			exclude: /node_modules/, | ||||
| 			use: [{ | ||||
| 				loader: 'ts-loader', | ||||
| 				options: { | ||||
| 					happyPackMode: true, | ||||
| 					transpileOnly: true, | ||||
| 					configFile: __dirname + '/tsconfig.json', | ||||
| 				} | ||||
| 			}] | ||||
| 		}] | ||||
| 	}, | ||||
| 	plugins: [ | ||||
| 		new webpack.ProgressPlugin({}), | ||||
| 		new webpack.DefinePlugin({ | ||||
| 			_VERSION_: JSON.stringify(meta.version), | ||||
| 			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), | ||||
| 			_ENV_: JSON.stringify(process.env.NODE_ENV), | ||||
| 			_DEV_: process.env.NODE_ENV !== 'production', | ||||
| 			_PERF_PREFIX_: JSON.stringify('Misskey:'), | ||||
| 		}), | ||||
| 	], | ||||
| 	output: { | ||||
| 		path: __dirname + '/../../built/_sw_dist_', | ||||
| 		filename: `[name].js`, | ||||
| 		publicPath: `/`, | ||||
| 		pathinfo: false, | ||||
| 	}, | ||||
| 	resolve: { | ||||
| 		extensions: [ | ||||
| 			'.js', '.ts', '.json' | ||||
| 		], | ||||
| 		alias: { | ||||
| 			'@': __dirname + '/src/', | ||||
| 		} | ||||
| 	}, | ||||
| 	resolveLoader: { | ||||
| 		modules: ['node_modules'] | ||||
| 	}, | ||||
| 	devtool: false, //'source-map',
 | ||||
| 	mode: isProduction ? 'production' : 'development' | ||||
| }; | ||||
|  | @ -3,7 +3,7 @@ const execa = require('execa'); | |||
| (async () => { | ||||
| 	console.log('installing dependencies of packages/backend ...'); | ||||
| 
 | ||||
| 	await execa('yarn', ['install'], { | ||||
| 	await execa('yarn', ['--force', 'install'], { | ||||
| 		cwd: __dirname + '/../packages/backend', | ||||
| 		stdout: process.stdout, | ||||
| 		stderr: process.stderr, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue