Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • TransFem-org/Sharkey
  • fEmber/Sharkey
  • kopper/Sharkey
  • dakkar/Sharkey-thenautilus
  • esm/Sharkey
  • blueb/Sharkey
  • aura/Sharkey
  • alicem/Sharkey
  • dimkr/Sharkey
  • vvinrg/Sharkey
  • tess/Sharkey
  • Latte_macchiato/Sharkey
  • skyevg/Sharkey
  • TransFem-org/sharkey-trans-fem
  • limepotato/sharkey
  • elrant/villkey
  • kio/kitsukey
  • 4censord/Sharkey
  • kanade/Sharkey
  • kakkokari-gtyih/Sharkey
  • piuvas/Sharkey
  • kio/gaykey
  • owlbear/Sharkey
  • esurio/Sharkey
  • cuteBoiButt/Sharkey
  • magi/Sharkey
  • chikorita157/Sharkey
  • CenTdemeern1/Sharkey
  • GeopJr/Sharkey
  • lhcfl/sharkey
  • cody/Sharkey
  • puppygirlhornypost/Sharkey
  • Sneexy/Sharkey
  • zotan/Sharkey
  • Marie/Sharkey
  • maciejla/Sharkey
  • transitory/Sharkey
  • Caramel/Sharkey
  • fly_mc/Sharkey
  • 87/Sharkey
  • echo/Sharkey
  • transfemsoc/Sharkey
  • ch0ccyra1n/Sharkey
  • interru/Sharkey
  • legiayayana/Sharkey
  • elizabeth-dev/Sharkey
  • JeDaYoshi/Sharkey
  • bunnybeam/Sharkey
  • and-then-there-were-two/Sharkey
  • easrng/Sharkey
  • HellhoundSoftware/Sharkey
  • realkinetix/Sharkey
  • sandervankasteel/Sharkey
  • jacobwhall/Sharkey
  • Daniel/Sharkey
  • rhythmkey/rhythmkey
56 results
Show changes
Showing
with 910 additions and 159 deletions
coverage:
status:
project: false
patch: false
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
services:
redis:
restart: always
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- ./redis:/data
healthcheck:
test: "redis-cli ping"
interval: 5s
retries: 20
db:
restart: always
image: postgres:15-alpine
ports:
- "5432:5432"
env_file:
- .config/docker.env
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
interval: 5s
retries: 20
# meilisearch:
# restart: always
# image: getmeili/meilisearch:v1.3.4
# environment:
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
# env_file:
# - .config/meilisearch.env
# volumes:
# - ./meili_data:/meili_data
services:
web:
# image: registry.activitypub.software/transfem-org/sharkey:latest
build: .
restart: always
links:
- db
- redis
# - mcaptcha
# - meilisearch
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3000:3000"
networks:
- shonk
# env_file:
# - .config/docker.env
environment:
- NODE_OPTIONS="--max-old-space-size=8192"
volumes:
- ./files:/sharkey/files
- ./.config:/sharkey/.config:ro
redis:
restart: always
image: redis:7-alpine
networks:
- shonk
volumes:
- ./redis:/data
healthcheck:
test: "redis-cli ping"
interval: 5s
retries: 20
db:
restart: always
image: groonga/pgroonga:4.0.1-alpine-17
networks:
- shonk
env_file:
- .config/docker.env
volumes:
- ./db:/var/lib/postgresql/data
healthcheck:
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
interval: 5s
retries: 20
# mcaptcha:
# restart: always
# image: mcaptcha/mcaptcha:latest
# networks:
# shonk:
# aliases:
# - localhost
# ports:
# - 7493:7493
# env_file:
# - .config/docker.env
# environment:
# PORT: 7493
# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
# MCAPTCHA_allow_registration: true
# MCAPTCHA_server_DOMAIN: "example.tld"
# depends_on:
# db:
# condition: service_healthy
# mcaptcha_redis:
# condition: service_healthy
#
# mcaptcha_redis:
# image: mcaptcha/cache:latest
# networks:
# - shonk
# healthcheck:
# test: "redis-cli ping"
# interval: 5s
# retries: 20
# meilisearch:
# restart: always
# image: getmeili/meilisearch:v1.13.0
# environment:
# - MEILI_NO_ANALYTICS=true
# - MEILI_ENV=production
# - MEILI_MASTER_KEY=ChangeThis
# networks:
# - shonk
# volumes:
# - ./meili_data:/meili_data
networks:
shonk:
files:
- source: /locales/ja-JP.yml
translation: /locales/%locale%.yml
- source: /sharkey-locales/en-US.yml
translation: /sharkey-locales/%locale%.yml
update_option: update_as_unapproved
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Before setup instance', () => {
beforeEach(() => {
cy.resetState();
......@@ -10,14 +15,15 @@ describe('Before setup instance', () => {
});
it('successfully loads', () => {
cy.visit('/');
cy.visitHome();
});
it('setup instance', () => {
cy.visit('/');
cy.visitHome();
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
cy.get('[data-cy-admin-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click();
......@@ -43,22 +49,51 @@ describe('After setup instance', () => {
});
it('successfully loads', () => {
cy.visit('/');
cy.visitHome();
});
it('signup', () => {
cy.visit('/');
cy.visitHome();
cy.intercept('POST', '/api/signup').as('signup');
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('not.be.disabled');
cy.get('[data-cy-signup-submit]').click();
cy.wait('@signup');
});
it('signup with duplicated username', () => {
cy.registerUser('alice', 'alice1234');
cy.visitHome();
// ユーザー名が重複している場合の挙動確認
cy.get('[data-cy-signup]').click();
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
cy.get('[data-cy-signup-rules-continue]').click();
cy.get('[data-cy-signup-username] input').type('alice');
cy.get('[data-cy-signup-password] input').type('alice1234');
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
cy.get('[data-cy-signup-submit]').should('be.disabled');
});
});
describe('After user signup', () => {
......@@ -79,17 +114,22 @@ describe('After user signup', () => {
});
it('successfully loads', () => {
cy.visit('/');
cy.visitHome();
});
it('signin', () => {
cy.visit('/');
cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin');
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
// Enterキーでサインインできるかの確認も兼ねる
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-username] input').type('alice{enter}');
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
// Enterキーで続行できるかの確認も兼ねる
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
......@@ -101,18 +141,19 @@ describe('After user signup', () => {
userId: this.alice.id,
});
cy.visit('/');
cy.visitHome();
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.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type('alice{enter}');
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
});
});
describe('After user singed in', () => {
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
......@@ -132,15 +173,80 @@ describe('After user singed in', () => {
});
it('successfully loads', () => {
cy.get('[data-cy-open-post-form]').should('be.visible');
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible');
});
it('account setup wizard', () => {
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click();
cy.get('[data-cy-user-setup-user-name] input').type('ありす');
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
// TODO: アイコン設定テスト
cy.get('[data-cy-user-setup-continue]').click();
// プライバシー設定
cy.get('[data-cy-user-setup-continue]').click();
// フォローはスキップ
cy.get('[data-cy-user-setup-continue]').click();
// プッシュ通知設定はスキップ
cy.get('[data-cy-user-setup-continue]').click();
cy.get('[data-cy-user-setup-continue]').click();
});
});
describe('After user setup', () => {
beforeEach(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
cy.get('[data-cy-modal-dialog-ok]').click();
});
afterEach(() => {
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
// waitを入れることでそれを防止できる
cy.wait(1000);
});
it('note', () => {
cy.get('[data-cy-open-post-form]').should('be.visible');
cy.get('[data-cy-open-post-form]').click();
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click();
cy.contains('Hello, Misskey!');
});
it('open note form with hotkey', () => {
// Wait until the page loads
cy.get('[data-cy-open-post-form]').should('be.visible');
// Use trigger() to give different `code` to test if hotkeys also work on non-QWERTY keyboards.
cy.document().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "n", code: "KeyL" });
// See if the form is opened
cy.get('[data-cy-post-form-text]').should('be.visible');
// Close it
cy.focused().trigger("keydown", { eventConstructor: 'KeyboardEvent', key: "Escape", code: "Escape" });
// See if the form is closed
cy.get('[data-cy-post-form-text]').should('not.be.visible');
});
});
......
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('Router transition', () => {
describe('Redirect', () => {
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
before(() => {
cy.resetState();
// インスタンス初期セットアップ
cy.registerUser('admin', 'pass', true);
// ユーザー作成
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click();
cy.wait(500);
cy.get('[data-cy-modal-dialog-ok]').click();
});
it('redirect to user profile', () => {
// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
cy.visit('/redirect-test');
// プロフィールページのURLであることを確認する
cy.url().should('include', '/@alice')
});
});
});
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* flaky
describe('After user signed in', () => {
beforeEach(() => {
cy.resetState();
......@@ -10,6 +16,10 @@ describe('After user signed in', () => {
cy.registerUser('alice', 'alice1234');
cy.login('alice', 'alice1234');
// アカウント初期設定ウィザード
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click();
cy.get('[data-cy-modal-dialog-ok]').click();
});
afterEach(() => {
......@@ -19,27 +29,27 @@ describe('After user signed in', () => {
});
it('widget edit toggle is visible', () => {
cy.get('.mk-widget-edit').should('be.visible');
cy.get('[data-cy-widget-edit]').should('be.visible');
});
it('widget select should be visible in edit mode', () => {
cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select').should('be.visible');
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-widget-select]').should('be.visible');
});
it('first widget should be removed', () => {
cy.get('.mk-widget-edit').click();
cy.get('.customize-container:first-child .remove._button').click();
cy.get('.customize-container').should('have.length', 2);
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-customize-container]:first-child [data-cy-customize-container-remove]._button').click();
cy.get('[data-cy-customize-container]').should('have.length', 2);
});
function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => {
cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select select').select(widgetName, { force: true });
cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true });
cy.get('.mk-widget-add').click({ force: true });
cy.get(`.mkw-${widgetName}`).should('exist');
cy.get('[data-cy-widget-edit]').click();
cy.get('[data-cy-widget-select] select').select(widgetName, { force: true });
cy.get('[data-cy-bg]._modalBg[data-cy-transparent]').click({ multiple: true, force: true });
cy.get('[data-cy-widget-add]').click({ force: true });
cy.get(`[data-cy-mkw-${widgetName}]`).should('exist');
});
}
......@@ -63,3 +73,4 @@ describe('After user signed in', () => {
buildWidgetTest('aiscript');
buildWidgetTest('aichan');
});
*/
......@@ -24,10 +24,19 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add('visitHome', () => {
cy.visit('/');
cy.get('button', { timeout: 30000 }).should('be.visible');
})
Cypress.Commands.add('resetState', () => {
cy.window(win => {
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
/*
cy.window().then(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);
......@@ -39,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
cy.request('POST', route, {
username: username,
password: password,
...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
}).its('body').as(username);
});
Cypress.Commands.add('login', (username, password) => {
cy.visit('/');
cy.visitHome();
cy.intercept('POST', '/api/signin').as('signin');
cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type(username);
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn');
......
......@@ -21,6 +21,8 @@ import './commands'
Cypress.on('uncaught:exception', (err, runnable) => {
if ([
'The source image cannot be decoded',
// Chrome
'ResizeObserver loop limit exceeded',
......
declare global {
namespace Cypress {
interface Chainable {
login(username: string, password: string): Chainable<void>;
registerUser(
username: string,
password: string,
isAdmin?: boolean
): Chainable<void>;
resetState(): Chainable<void>;
visitHome(): Chainable<void>;
}
}
}
export {}
{
"compilerOptions": {
"lib": ["dom", "es5"],
"target": "es5",
"types": ["cypress", "node"]
},
"include": ["./**/*.ts"]
}
version: "3"
services:
web:
build: .
restart: always
links:
- db
- redis
# - es
ports:
- "3000:3000"
networks:
- internal_network
- external_network
volumes:
- ./files:/misskey/files
- ./.config:/misskey/.config:ro
redis:
restart: always
image: redis:4.0-alpine
networks:
- internal_network
volumes:
- ./redis:/data
db:
restart: always
image: postgres:12.2-alpine
networks:
- internal_network
env_file:
- .config/docker.env
volumes:
- ./db:/var/lib/postgresql/data
# es:
# restart: always
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
# environment:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
# - "TAKE_FILE_OWNERSHIP=111"
# networks:
# - internal_network
# volumes:
# - ./elasticsearch:/usr/share/elasticsearch/data
networks:
internal_network:
internal: true
external_network:
DONATORS
========
The list of people who have sent donation for Misskey.
(In random order, honorific titles are omitted.)
* らふぁ
* 俺様
* なぎうり
* スルメ https://surume.tk/
*
* 音船 https://otofune.me/
* aqz https://misskey.xyz/aqz
* kotodu "虚無創作中"
* Maya Minatsuki
* Knzk https://knzk.me/@Knzk
* ねじりわさび https://knzk.me/@y
* NCLS https://knzk.me/@imncls]
* こじま @skoji@sandbox.skoji.jp
:heart: Thanks for donating, guys!
---
If your name is missing, please contact us!
/*
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx`
* objects that reference translation items that don't actually exist
* in the lexicon (the `locale/` files)
*/
/* given a MemberExpression node, collects all the member names
*
* e.g. for a bit of code like `foo=one.two.three`, `collectMembers`
* called on the node for `three` would return `['one', 'two',
* 'three']`
*/
function collectMembers(node) {
if (!node) return [];
if (node.type !== 'MemberExpression') return [];
// this is something like `foo[bar]`
if (node.computed) return [];
return [ node.property.name, ...collectMembers(node.parent) ];
}
/* given an object and an array of names, recursively descends the
* object via those names
*
* e.g. `walkDown({one:{two:{three:15}}},['one','two','three'])` would
* return 15
*/
function walkDown(locale, path) {
if (!locale) return null;
if (!path || path.length === 0 || !path[0]) return locale;
return walkDown(locale[path[0]], path.slice(1));
}
/* given a MemberExpression node, returns its attached CallExpression
* node if present
*
* e.g. for a bit of code like `foo=one.two.three()`,
* `findCallExpression` called on the node for `three` would return
* the node for function call (which is the parent of the `one` and
* `two` nodes, and holds the nodes for the argument list)
*
* if the code had been `foo=one.two.three`, `findCallExpression`
* would have returned null, because there's no function call attached
* to the MemberExpressions
*/
function findCallExpression(node) {
if (!node.parent) return null;
// the second half of this guard protects from cases like
// `foo(one.two.three)` where the CallExpression is parent of the
// MemberExpressions, but via `arguments`, not `callee`
if (node.parent.type === 'CallExpression' && node.parent.callee === node) return node.parent;
if (node.parent.type === 'MemberExpression') return findCallExpression(node.parent);
return null;
}
// same, but for Vue expressions (`<I18n :src="i18n.ts.foo">`)
function findVueExpression(node) {
if (!node.parent) return null;
if (node.parent.type.match(/^VExpr/) && node.parent.expression === node) return node.parent;
if (node.parent.type === 'MemberExpression') return findVueExpression(node.parent);
return null;
}
function areArgumentsOneObject(node) {
return node.arguments.length === 1 &&
node.arguments[0].type === 'ObjectExpression';
}
// only call if `areArgumentsOneObject(node)` is true
function getArgumentObjectProperties(node) {
return new Set(node.arguments[0].properties.map(
p => {
if (p.key && p.key.type === 'Identifier') return p.key.name;
return null;
},
));
}
function getTranslationParameters(translation) {
return new Set(Array.from(translation.matchAll(/\{(\w+)\}/g)).map( m => m[1] ));
}
function setDifference(a,b) {
const result = [];
for (const element of a.values()) {
if (!b.has(element)) {
result.push(element);
}
}
return result;
}
/* the actual rule body
*/
function theRuleBody(context,node) {
// we get the locale/translations via the options; it's the data
// that goes into a specific language's JSON file, see
// `scripts/build-assets.mjs`
const locale = context.options[0];
// sometimes we get MemberExpression nodes that have a
// *descendent* with the right identifier: skip them, we'll get
// the right ones as well
if (node.object?.name !== 'i18n') {
return;
}
// `method` is going to be `'ts'` or `'tsx'`, `path` is going to
// be the various translation steps/names
const [ method, ...path ] = collectMembers(node);
const pathStr = `i18n.${method}.${path.join('.')}`;
// does that path point to a real translation?
const translation = walkDown(locale, path);
if (!translation) {
context.report({
node,
message: `translation missing for ${pathStr}`,
});
return;
}
// we hit something weird, assume the programmers know what
// they're doing (this is usually some complicated slicing of
// the translation structure)
if (typeof(translation) !== 'string') return;
const callExpression = findCallExpression(node);
const vueExpression = findVueExpression(node);
// some more checks on how the translation is called
if (method === 'ts') {
// the `<I18n> component gets parametric translations via
// `i18n.ts.*`, but we error out elsewhere
if (translation.match(/\{/) && !vueExpression) {
context.report({
node,
message: `translation for ${pathStr} is parametric, but called via 'ts'`,
});
return;
}
if (callExpression) {
context.report({
node,
message: `translation for ${pathStr} is not parametric, but is called as a function`,
});
}
}
if (method === 'tsx') {
if (!translation.match(/\{/)) {
context.report({
node,
message: `translation for ${pathStr} is not parametric, but called via 'tsx'`,
});
return;
}
if (!callExpression && !vueExpression) {
context.report({
node,
message: `translation for ${pathStr} is parametric, but not called as a function`,
});
return;
}
// we're not currently checking arguments when used via the
// `<I18n>` component, because it's too complicated (also, it
// would have to be done inside the `if (method === 'ts')`)
if (!callExpression) return;
if (!areArgumentsOneObject(callExpression)) {
context.report({
node,
message: `translation for ${pathStr} should be called with a single object as argument`,
});
return;
}
const translationParameters = getTranslationParameters(translation);
const parameterCount = translationParameters.size;
const callArguments = getArgumentObjectProperties(callExpression);
const argumentCount = callArguments.size;
if (parameterCount !== argumentCount) {
context.report({
node,
message: `translation for ${pathStr} has ${parameterCount} parameters, but is called with ${argumentCount} arguments`,
});
}
// node 20 doesn't have `Set.difference`...
const extraArguments = setDifference(callArguments, translationParameters);
const missingArguments = setDifference(translationParameters, callArguments);
if (extraArguments.length > 0) {
context.report({
node,
message: `translation for ${pathStr} passes unused arguments ${extraArguments.join(' ')}`,
});
}
if (missingArguments.length > 0) {
context.report({
node,
message: `translation for ${pathStr} does not pass arguments ${missingArguments.join(' ')}`,
});
}
}
}
function theRule(context) {
// we get the locale/translations via the options; it's the data
// that goes into a specific language's JSON file, see
// `scripts/build-assets.mjs`
const locale = context.options[0];
// for all object member access that have an identifier 'i18n'...
return context.getSourceCode().parserServices.defineTemplateBodyVisitor(
{
// this is for <template> bits, needs work
'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node),
},
{
// this is for normal code
'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node),
},
);
}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'assert that all translations used are present in the locale files',
},
schema: [
// here we declare that we need the locale/translation as a
// generic object
{ type: 'object', additionalProperties: true },
],
},
create: theRule,
};
/*
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
const {RuleTester} = require("eslint");
const localeRule = require("./locale");
const locale = { foo: { bar: 'ok', baz: 'good {x}' }, top: '123' };
const ruleTester = new RuleTester({
languageOptions: {
parser: require('vue-eslint-parser'),
ecmaVersion: 2015,
},
});
function testCase(code,errors) {
return { code, errors, options: [ locale ], filename: 'test.ts' };
}
function testCaseVue(code,errors) {
return { code, errors, options: [ locale ], filename: 'test.vue' };
}
ruleTester.run(
'sharkey-locale',
localeRule,
{
valid: [
testCase('i18n.ts.foo.bar'),
testCase('i18n.ts.top'),
testCase('i18n.tsx.foo.baz({x:1})'),
testCase('whatever.i18n.ts.blah.blah'),
testCase('whatever.i18n.tsx.does.not.matter'),
testCase('whatever(i18n.ts.foo.bar)'),
testCaseVue('<template><p>{{ i18n.ts.foo.bar }}</p></template>'),
testCaseVue('<template><I18n :src="i18n.ts.foo.baz"/></template>'),
// we don't detect the problem here, but should still accept it
testCase('i18n.ts.foo["something"]'),
testCase('i18n.ts.foo[something]'),
],
invalid: [
testCase('i18n.ts.not', 1),
testCase('i18n.tsx.deep.not', 1),
testCase('i18n.tsx.deep.not({x:12})', 1),
testCase('i18n.tsx.top({x:1})', 1),
testCase('i18n.ts.foo.baz', 1),
testCase('i18n.tsx.foo.baz', 1),
testCase('i18n.tsx.foo.baz({y:2})', 2),
testCaseVue('<template><p>{{ i18n.ts.not }}</p></template>', 1),
testCaseVue('<template><I18n :src="i18n.ts.not"/></template>', 1),
],
},
);
/**
* Gulp tasks
*/
const fs = require('fs');
const gulp = require('gulp');
const replace = require('gulp-replace');
const terser = require('gulp-terser');
const cssnano = require('gulp-cssnano');
const locales = require('./locales');
const meta = require('./package.json');
gulp.task('copy:backend:views', () =>
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
);
gulp.task('copy:frontend:fonts', () =>
gulp.src('./packages/frontend/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_frontend_dist_/fonts/'))
);
gulp.task('copy:frontend:tabler-icons', () =>
gulp.src('./packages/frontend/node_modules/@tabler/icons/iconfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/'))
);
gulp.task('copy:frontend:locales', cb => {
fs.mkdirSync('./built/_frontend_dist_/locales', { recursive: true });
const v = { '_version_': meta.version };
for (const [lang, locale] of Object.entries(locales)) {
fs.writeFileSync(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
}
cb();
});
gulp.task('build:backend:script', () => {
return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js'])
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
.pipe(terser({
toplevel: true
}))
.pipe(gulp.dest('./packages/backend/built/server/web/'));
});
gulp.task('build:backend:style', () => {
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
.pipe(cssnano({
zindex: false
}))
.pipe(gulp.dest('./packages/backend/built/server/web/'));
});
gulp.task('build', gulp.parallel(
'copy:frontend:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:frontend:fonts', 'copy:frontend:tabler-icons'
));
gulp.task('default', gulp.task('build'));
gulp.task('watch', () => {
gulp.watch([
'./packages/*/src/**/*',
], { ignoreInitial: false }, gulp.task('build'));
});
#!/bin/bash
# SPDX-FileCopyrightText: syuilo and misskey-project
# SPDX-License-Identifier: AGPL-3.0-only
PORT=$(grep '^port:' /sharkey/.config/default.yml | awk 'NR==1{print $2; exit}')
curl -Sfso/dev/null "http://localhost:${PORT}/healthz"
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
export const Default = {
render(args) {
return {
components: {
MkAbuseReport,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
resolved: action('resolved'),
};
},
},
template: '<MkAbuseReport v-bind="props" v-on="events" />',
};
},
args: {
report: abuseUserReport(),
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
return HttpResponse.json({});
}),
],
},
},
} satisfies StoryObj<typeof MkAbuseReport>;
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root]">
<div :inert="disabled" :class="[{ [$style.disabled]: disabled }]">
<slot></slot>
</div>
<div v-if="disabled" :class="[$style.cover]"></div>
</div>
</template>
<script lang="ts" setup>
defineProps<{
disabled?: boolean;
}>();
</script>
<style lang="scss" module>
.root {
position: relative;
}
.disabled {
opacity: 0.7;
}
.cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: not-allowed;
--color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
}
</style>
使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ