Merge branch 'develop' into release/2024-03-30

This commit is contained in:
dakkar 2024-03-30 11:08:26 +00:00
commit 328546c4cd
24 changed files with 269 additions and 118 deletions

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MoreRepoUrl1709462550083 {
name = 'MoreRepoUrl1709462550083'
async up(queryRunner) {
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl"=DEFAULT WHERE "repositoryUrl" IN ('https://git.joinfirefish.org/firefish/firefish','https://codeberg/firefish/firefish','https://codeberg.org/calckey/calckey','https://iceshrimp.dev/iceshrimp/iceshrimp')`);
await queryRunner.query(`UPDATE "meta" SET "feedbackUrl"=DEFAULT WHERE "feedbackUrl" IN ('https://git.joinfirefish.org/firefish/firefish/issues','https://codeberg/firefish/firefish/issues','https://codeberg.org/calckey/calckey/firefish/firefish/issues','https://iceshrimp.dev/iceshrimp/iceshrimp/issues/new','https://iceshrimp.dev/iceshrimp/iceshrimp/issues')`);
}
async down(queryRunner) {
}
}

View file

@ -11,7 +11,11 @@ export default new DataSource({
username: config.db.user,
password: config.db.pass,
database: config.db.db,
extra: config.db.extra,
extra: {
...config.db.extra,
// migrations may be very slow, give them longer to run (that 10*1000 comes from postgres.ts)
statement_timeout: (config.db.extra?.statement_timeout ?? 1000 * 10) * 10,
},
entities: entities,
migrations: ['migration/*.js'],
});

View file

@ -15,6 +15,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
import type { IObject } from '@/core/activitypub/type.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
@ -125,7 +126,12 @@ export class HttpRequestService {
validators: [validateContentTypeSetAsActivityPub],
});
return await res.json() as IObject;
const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(activity, [url, finalUrl]);
return activity;
}
@bindThis

View file

@ -266,6 +266,16 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
const hasProhibitedWords = await this.checkProhibitedWordsContain({
cw: data.cw,
text: data.text,
pollChoices: data.poll?.choices,
}, meta.prohibitedWords);
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
}
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
@ -299,7 +309,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Check blocking
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
if (data.renote && !this.isQuote(data)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@ -1018,7 +1028,7 @@ export class NoteCreateService implements OnApplicationShutdown {
removeOnComplete: true,
});
}
// Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });

View file

@ -86,7 +86,7 @@ export class UtilityService {
@bindThis
public extractDbHost(uri: string): string {
const url = new URL(uri);
return this.toPuny(url.hostname);
return this.toPuny(url.host);
}
@bindThis
@ -99,4 +99,11 @@ export class UtilityService {
if (host == null) return null;
return toASCII(host.toLowerCase());
}
@bindThis
public punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
}

View file

@ -14,7 +14,9 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import type { IObject } from './type.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
type Request = {
url: string;
@ -201,6 +203,11 @@ export class ApRequestService {
validators: [validateContentTypeSetAsActivityPub],
});
return await res.json();
const finalUrl = res.url; // redirects may have been involved
const activity = await res.json() as IObject;
assertActivityMatchesUrls(activity, [url, finalUrl]);
return activity;
}
}

View file

@ -115,6 +115,14 @@ export class Resolver {
throw new Error('invalid response');
}
// HttpRequestService / ApRequestService have already checked that
// `object.id` or `object.url` matches the URL used to fetch the
// object after redirects; here we double-check that no redirects
// bounced between hosts
if (object.id && (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value))) {
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
}
return object;
}

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: dakkar and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { IObject } from '../type.js';
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
const idOk = activity.id !== undefined && urls.includes(activity.id);
// technically `activity.url` could be an `ApObject = IObject |
// string | (IObject | string)[]`, but if it's a complicated thing
// and the `activity.id` doesn't match, I think we're fine
// rejecting the activity
const urlOk = typeof(activity.url) === 'string' && urls.includes(activity.url);
if (!idOk && !urlOk) {
throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`);
}
}

View file

@ -127,12 +127,6 @@ export class ApPersonService implements OnModuleInit {
this.logger = this.apLoggerService.logger;
}
private punyHost(url: string): string {
const urlObj = new URL(url);
const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
return host;
}
/**
* Validate and convert to actor object
* @param x Fetched object
@ -140,7 +134,7 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
private validateActor(x: IObject, uri: string): IActor {
const expectHost = this.punyHost(uri);
const expectHost = this.utilityService.punyHost(uri);
if (!isActor(x)) {
throw new Error(`invalid Actor type '${x.type}'`);
@ -154,6 +148,19 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: wrong inbox');
}
if (this.utilityService.punyHost(x.inbox) !== expectHost) {
throw new Error('invalid Actor: inbox has different host');
}
for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) {
const collectionUri = (x as IActor)[collection];
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
if (this.utilityService.punyHost(collectionUri) !== expectHost) {
throw new Error(`invalid Actor: ${collection} has different host`);
}
}
}
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
throw new Error('invalid Actor: wrong username');
}
@ -177,7 +184,7 @@ export class ApPersonService implements OnModuleInit {
x.summary = truncate(x.summary, summaryLength);
}
const idHost = this.punyHost(x.id);
const idHost = this.utilityService.punyHost(x.id);
if (idHost !== expectHost) {
throw new Error('invalid Actor: id has different host');
}
@ -187,7 +194,7 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: publicKey.id is not a string');
}
const publicKeyIdHost = this.punyHost(x.publicKey.id);
const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
if (publicKeyIdHost !== expectHost) {
throw new Error('invalid Actor: publicKey.id has different host');
}
@ -286,7 +293,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Creating the Person: ${person.id}`);
const host = this.punyHost(object.id);
const host = this.utilityService.punyHost(object.id);
const fields = this.analyzeAttachments(person.attachment ?? []);

View file

@ -113,8 +113,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const host = this.utilityService.extractDbHost(uri);
const fetchedMeta = await this.metaService.fetch();
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@ -122,6 +123,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
// local object, not found in db? fail
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri) as any;

View file

@ -43,6 +43,7 @@ export async function signout() {
waiting();
miLocalStorage.removeItem('account');
await removeAccount($i.id);
document.cookie = `token=; path=/; max-age=0${ location.protocol === 'https:' ? '; Secure' : ''}`;
const accounts = await getAccounts();
//#region Remove service worker registration
@ -200,7 +201,7 @@ export async function login(token: Account['token'], redirect?: string) {
throw reason;
});
miLocalStorage.setItem('account', JSON.stringify(me));
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
document.cookie = `token=${token}; path=/; max-age=31536000${ location.protocol === 'https:' ? '; Secure' : ''}`; // bull dashboardの認証とかで使う
await addAccount(me.id, token);
if (redirect) {

View file

@ -73,27 +73,31 @@ export async function mainBoot() {
mainRouter.push('/search');
},
};
if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1;
if (defaultStore.state.hemisphere === 'S') {
// ▼南半球
if (month === 7 || month === 8) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect({}).render();
try {
if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1;
if (defaultStore.state.hemisphere === 'S') {
// ▼南半球
if (month === 7 || month === 8) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect({}).render();
}
} else {
// ▼北半球
if (month === 12 || month === 1) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect({}).render();
} else if (month === 3 || month === 4) {
const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SakuraEffect({
sakura: true,
}).render();
}
}
} else {
// ▼北半球
if (month === 12 || month === 1) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect({}).render();
} else if (month === 3 || month === 4) {
const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SakuraEffect({
sakura: true,
}).render();
}
}
}
} catch (error) {
// console.error(error);
console.error('Failed to initialise the seasonal screen effect canvas context:', error);
}
if ($i) {

View file

@ -72,12 +72,16 @@ watch(() => props.lang, (to) => {
</script>
<style module lang="scss">
.codeBlockRoot {
text-align: left;
}
.codeBlockRoot :global(.shiki) > code {
counter-reset: step;
counter-increment: step 0;
}
.codeBlockRoot :global(.shiki) > code > .line::before {
.codeBlockRoot :global(.shiki) > code > span::before {
content: counter(step);
counter-increment: step;
width: 1rem;

View file

@ -20,7 +20,7 @@
worker-src 'self';
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
frame-src *;"

View file

@ -77,14 +77,34 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
<div :class="$style.contributors" style="margin-bottom: 8px;">
<a href="https://activitypub.software/Marie" target="_blank" :class="$style.contributor">
<img src="https://activitypub.software/uploads/-/system/user/avatar/2/avatar.png?width=128" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@Marie</span>
</a>
<a href="https://activitypub.software/Amelia" target="_blank" :class="$style.contributor">
<img src="https://activitypub.software/uploads/-/system/user/avatar/1/avatar.png?width=128" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@Amelia</span>
</a>
<a href="https://activitypub.software/dakkar" target="_blank" :class="$style.contributor">
<img src="https://secure.gravatar.com/avatar/c71b315eed7c63ff94c42b1b3e8dbad1?s=192&d=identicon" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@dakkar</span>
</a>
<a href="https://activitypub.software/esm" target="_blank" :class="$style.contributor">
<img src="https://secure.gravatar.com/avatar/00fd054610e2a9dcf97a2aa661b168d0?s=192&d=identicon" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@esm</span>
</a>
<a href="https://activitypub.software/supakaity" target="_blank" :class="$style.contributor">
<img src="https://activitypub.software/uploads/-/system/user/avatar/65/avatar.png?width=40" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@supakaity</span>
</a>
<a href="https://activitypub.software/julia" target="_blank" :class="$style.contributor">
<img src="https://activitypub.software/uploads/-/system/user/avatar/41/avatar.png?width=40" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@julia</span>
</a>
<a href="https://activitypub.software/Leah" target="_blank" :class="$style.contributor">
<img src="https://secure.gravatar.com/avatar/3b35b921b284ccfd1fe348508f6f705b?s=80&d=identicon" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@Leah</span>
</a>
<a href="https://activitypub.software/fEmber" target="_blank" :class="$style.contributor">
<img src="https://secure.gravatar.com/avatar/ea0ea6451fdb74311efad369bdce018e?s=80&d=identicon" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@fEmber</span>
</a>
</div>
<template #caption><MkLink url="https://activitypub.software/TransFem-org/Sharkey/-/graphs/develop">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
</FormSection>

View file

@ -85,7 +85,7 @@ async function search() {
if (query == null || query === '') return;
if (query.startsWith('https://')) {
if (query.startsWith('http://') || query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
});

View file

@ -48,7 +48,7 @@ async function search() {
if (query == null || query === '') return;
if (query.startsWith('https://')) {
if (query.startsWith('http://') || query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
});

View file

@ -40,7 +40,7 @@ const isScrolling = ref(false);
const scrollEl = shallowRef<HTMLElement>();
misskeyApiGet('notes/featured').then(_notes => {
notes.value = _notes;
notes.value = _notes.filter(n => n.cw == null);
});
onUpdated(() => {

View file

@ -28,7 +28,7 @@ export async function lookup(router?: Router) {
return;
}
if (query.startsWith('https://')) {
if (query.startsWith('http://') || query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
});

View file

@ -155,7 +155,9 @@ export class SnowfallEffect {
max: 0.125,
easing: 0.0005,
};
/**
* @throws {Error} - Thrown when it fails to get WebGL context for the canvas
*/
constructor(options: {
sakura?: boolean;
}) {