feat: [frontend / backend] update git urls, update userid watermark, authenticate image proxy and url previews
This commit is contained in:
@@ -6,8 +6,8 @@
|
||||
export class AddSomeUrls1557761316509 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "ToSUrl" character varying(512)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://git.leafus.net/pawkey/pawkey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://git.leafus.net/pawkey/pawkey/issues/new/choose'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://git.pawlickers.org/pawkey/pawkey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://git.pawlickers.org/pawkey/pawkey/issues/new'`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: pawinput and Pawkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class ChangeGITUrls1763436210524 {
|
||||
name = 'ChangeGITUrls1763436210524'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://git.pawlickers.org/pawkey/pawkey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://git.pawlickers.org/pawkey/pawkey/issues/new'`);
|
||||
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl"=DEFAULT WHERE "repositoryUrl" IN ('https://git.leafus.net/pawkey/pawkey')`);
|
||||
await queryRunner.query(`UPDATE "meta" SET "feedbackUrl"=DEFAULT WHERE "feedbackUrl" IN ('https://git.leafus.net/pawkey/pawkey/-/issues/new')`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://git.leafus.net/pawkey/pawkey'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://git.leafus.net/pawkey/pawkey/-/issues/new'`);
|
||||
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl"=DEFAULT WHERE "repositoryUrl" IN ('https://git.pawlickers.org/pawkey/pawkey')`);
|
||||
await queryRunner.query(`UPDATE "meta" SET "feedbackUrl"=DEFAULT WHERE "feedbackUrl" IN ('https://git.pawlickers.org/pawkey/pawkey/issues/new')`);
|
||||
}
|
||||
}
|
||||
@@ -483,7 +483,7 @@ export function loadConfig(): Config {
|
||||
videoThumbnailGenerator: config.videoThumbnailGenerator ?
|
||||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||
: null,
|
||||
userAgent: `Misskey/${version} (${config.url})`,
|
||||
userAgent: `Pawkey/${version} (${config.url})`,
|
||||
frontendEntry: frontendManifest['src/_boot_.ts'],
|
||||
frontendManifestExists: frontendManifestExists,
|
||||
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
|
||||
|
||||
@@ -458,14 +458,14 @@ export class MiMeta {
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
default: 'https://git.leafus.net/pawkey/pawkey/',
|
||||
default: 'https://git.pawlickers.org/pawkey/pawkey/',
|
||||
nullable: false,
|
||||
})
|
||||
public repositoryUrl: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
default: 'https://git.leafus.net/pawkey/pawkey/-/issues/new',
|
||||
default: 'https://git.pawlickers.org/pawkey/pawkey/issues/new',
|
||||
nullable: true,
|
||||
})
|
||||
public feedbackUrl: string | null;
|
||||
|
||||
@@ -108,6 +108,43 @@ export class FileServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
|
||||
let cookieToken: string | undefined;
|
||||
if (request.headers.cookie) {
|
||||
const cookies = request.headers.cookie.split(';').reduce((acc, cookie) => {
|
||||
const [key, value] = cookie.trim().split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
cookieToken = cookies.token;
|
||||
}
|
||||
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: (body as any)?.['i'] ?? cookieToken;
|
||||
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const [user] = await this.authenticateService.authenticate(token);
|
||||
|
||||
if (!user) {
|
||||
reply.code(401);
|
||||
reply.send({
|
||||
error: {
|
||||
message: 'Credential required.',
|
||||
code: 'CREDENTIAL_REQUIRED',
|
||||
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keyUrl = new URL(url);
|
||||
keyUrl.searchParams.forEach(k => keyUrl.searchParams.delete(k));
|
||||
keyUrl.hash = '';
|
||||
@@ -632,7 +669,7 @@ export class FileServerService {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: body?.['i'];
|
||||
: (body as any)?.['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return false;
|
||||
|
||||
@@ -95,12 +95,27 @@ export class StreamingApiServerService {
|
||||
? request.headers.authorization.slice(7)
|
||||
: q.get('i');
|
||||
|
||||
// Require authentication for WebSocket connections
|
||||
if (!token) {
|
||||
socket.write([
|
||||
'HTTP/1.1 401 Unauthorized',
|
||||
'WWW-Authenticate: Bearer realm="Misskey"',
|
||||
].join('\r\n') + '\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
[user, app] = await this.authenticateService.authenticate(token);
|
||||
|
||||
if (app !== null && !app.permission.some(p => p === 'read:account')) {
|
||||
throw new AuthenticationError('Your app does not have necessary permissions to use websocket API.');
|
||||
}
|
||||
|
||||
// Ensure we have a valid user
|
||||
if (!user) {
|
||||
throw new AuthenticationError('Invalid token or user not found.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof AuthenticationError) {
|
||||
socket.write([
|
||||
|
||||
@@ -19,7 +19,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
||||
|
||||
externalDocs: {
|
||||
description: 'Repository',
|
||||
url: 'https://git.leafus.net/pawkey/pawkey',
|
||||
url: 'https://git.pawlickers.org/pawkey/pawkey',
|
||||
},
|
||||
|
||||
servers: [{
|
||||
@@ -97,7 +97,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
||||
description: desc,
|
||||
externalDocs: {
|
||||
description: 'Source code',
|
||||
url: `https://git.leafus.net/pawkey/pawkey/-/tree/stable/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
|
||||
url: `https://git.pawlickers.org/pawkey/pawkey/src/branch/stable/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
|
||||
},
|
||||
...(endpoint.meta.tags ? {
|
||||
tags: [endpoint.meta.tags[0]],
|
||||
|
||||
@@ -169,8 +169,22 @@ export class UrlPreviewService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
// Check authentication
|
||||
const auth = await this.authenticate(request);
|
||||
const [user] = auth;
|
||||
|
||||
if (!user) {
|
||||
reply.code(401);
|
||||
return reply.send({
|
||||
error: {
|
||||
message: 'Credential required.',
|
||||
code: 'CREDENTIAL_REQUIRED',
|
||||
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
if (!await this.checkRateLimit(auth, reply)) {
|
||||
return;
|
||||
}
|
||||
@@ -450,10 +464,21 @@ export class UrlPreviewService {
|
||||
private async authenticate(request: FastifyRequest<{ Querystring?: { i?: string | string[] }, Body?: { i?: string | string[] } }>): Promise<AuthArray> {
|
||||
const body = request.method === 'GET' ? request.query : request.body;
|
||||
|
||||
// Parse token from cookie header if present
|
||||
let cookieToken: string | undefined;
|
||||
if (request.headers.cookie) {
|
||||
const cookies = request.headers.cookie.split(';').reduce((acc, cookie) => {
|
||||
const [key, value] = cookie.trim().split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
cookieToken = cookies.token;
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
|
||||
const token = request.headers.authorization?.startsWith('Bearer ')
|
||||
? request.headers.authorization.slice(7)
|
||||
: body?.['i'];
|
||||
: body?.['i'] ?? cookieToken;
|
||||
if (token != null && typeof token !== 'string') {
|
||||
return [undefined, undefined, getIpHash(request.ip)];
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 774 KiB |
@@ -16,6 +16,7 @@ import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { signout } from '@/signout.js';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
|
||||
|
||||
@@ -186,6 +187,8 @@ export async function login(token: AccountWithToken['token'], redirect?: string)
|
||||
token,
|
||||
}));
|
||||
|
||||
Cookies.set('token', token, { expires: 365 * 100 });
|
||||
|
||||
await addAccount(host, me, token);
|
||||
|
||||
if (redirect) {
|
||||
|
||||
@@ -335,7 +335,7 @@ export async function mainBoot() {
|
||||
}
|
||||
|
||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://git.leafus.net/pawkey/pawkey') {
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://git.pawlickers.org/pawkey/pawkey') {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
|
||||
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
|
||||
|
||||
<a href="https://git.leafus.net/pawkey/pawkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
|
||||
<a href="https://git.pawlickers.org/pawkey/pawkey/src/branch/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
|
||||
|
||||
<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
@@ -27,7 +27,7 @@ const modal = useTemplateRef('modal');
|
||||
|
||||
const whatIsNew = () => {
|
||||
modal.value?.close();
|
||||
window.open(`https://git.leafus.net/pawkey/pawkey/-/releases/${version}`, '_blank');
|
||||
window.open(`https://git.pawlickers.org/pawkey/pawkey/releases${version}`, '_blank');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { $i } from "@/i.js";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
const generateRandomClass = () => {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
@@ -14,10 +14,27 @@ const generateRandomClass = () => {
|
||||
const gridClass = ref(generateRandomClass());
|
||||
const itemClasses = ref<string[]>([]);
|
||||
const recreationKey = ref(0);
|
||||
const gridItemCount = ref(600);
|
||||
let regenerationInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let styleElement: HTMLStyleElement | null = null;
|
||||
let allCreatedClasses: string[] = [];
|
||||
|
||||
// Calculate grid items based on screen resolution
|
||||
const calculateGridItemCount = () => {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
// Grid cell size: 80px width + 10px gap, 16px height + 10px gap (more concise)
|
||||
const itemWidth = 80 + 10;
|
||||
const itemHeight = 16 + 10;
|
||||
|
||||
const columns = Math.ceil(width / itemWidth);
|
||||
const rows = Math.ceil(height / itemHeight);
|
||||
|
||||
// Add some buffer (20%) to ensure coverage during animations/scrolling
|
||||
return Math.ceil(columns * rows * 1.2);
|
||||
};
|
||||
|
||||
const encodeTextToPath = (text: string) => {
|
||||
const charPaths: { [key: string]: string } = {
|
||||
a: "M2,8 L4,2 L6,2 L8,8 M3,6 L7,6",
|
||||
@@ -77,7 +94,7 @@ const encodeTextToPath = (text: string) => {
|
||||
);
|
||||
path += translatedPath + " ";
|
||||
}
|
||||
xOffset += 10;
|
||||
xOffset += 7; // Reduced from 10 to 7 for tighter spacing
|
||||
});
|
||||
|
||||
return path.trim();
|
||||
@@ -95,7 +112,9 @@ const regenerateWatermark = () => {
|
||||
cleanupOldElements();
|
||||
|
||||
gridClass.value = generateRandomClass();
|
||||
itemClasses.value = generateItemClasses(600);
|
||||
const count = calculateGridItemCount();
|
||||
gridItemCount.value = count;
|
||||
itemClasses.value = generateItemClasses(count);
|
||||
recreationKey.value++;
|
||||
|
||||
allCreatedClasses.push(gridClass.value, ...itemClasses.value);
|
||||
@@ -140,13 +159,13 @@ const createStyleElement = () => {
|
||||
.${className} svg {
|
||||
opacity: 0.02 !important;
|
||||
width: auto !important;
|
||||
height: 12px !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
.${className} svg path {
|
||||
stroke: var(--MI_THEME-fg) !important;
|
||||
fill: none !important;
|
||||
stroke-width: 1 !important;
|
||||
stroke-width: 0.8 !important;
|
||||
}
|
||||
`,
|
||||
)
|
||||
@@ -160,10 +179,10 @@ const createStyleElement = () => {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important;
|
||||
grid-template-rows: repeat(auto-fill, minmax(20px, 1fr)) !important;
|
||||
gap: 20px !important;
|
||||
padding: 20px !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)) !important;
|
||||
grid-template-rows: repeat(auto-fill, minmax(16px, 1fr)) !important;
|
||||
gap: 10px !important;
|
||||
padding: 10px !important;
|
||||
pointer-events: none !important;
|
||||
z-index: 999999999 !important;
|
||||
overflow: hidden !important;
|
||||
@@ -177,24 +196,51 @@ const createStyleElement = () => {
|
||||
|
||||
const watermarkText = computed(() => $i?.id || "not signed in");
|
||||
const pathData = computed(() => encodeTextToPath(watermarkText.value));
|
||||
const textWidth = computed(() => watermarkText.value.length * 10 + 10);
|
||||
const textWidth = computed(() => watermarkText.value.length * 7 + 10); // Adjusted for tighter spacing
|
||||
|
||||
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const handleResize = () => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
|
||||
// Debounce resize to avoid excessive recalculations
|
||||
resizeTimeout = setTimeout(() => {
|
||||
regenerateWatermark();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
itemClasses.value = generateItemClasses(600);
|
||||
const count = calculateGridItemCount();
|
||||
gridItemCount.value = count;
|
||||
itemClasses.value = generateItemClasses(count);
|
||||
allCreatedClasses.push(gridClass.value, ...itemClasses.value);
|
||||
createStyleElement();
|
||||
|
||||
regenerationInterval = setInterval(() => {
|
||||
itemClasses.value = generateItemClasses(600);
|
||||
const count = calculateGridItemCount();
|
||||
gridItemCount.value = count;
|
||||
itemClasses.value = generateItemClasses(count);
|
||||
allCreatedClasses.push(gridClass.value, ...itemClasses.value);
|
||||
regenerateWatermark();
|
||||
}, 5000);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (regenerationInterval) clearInterval(regenerationInterval);
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cleanupOldElements();
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :key="`watermark-${recreationKey}`" :class="gridClass">
|
||||
<div v-for="n in 600" :key="n" :class="itemClasses[n]">
|
||||
<div v-for="n in gridItemCount" :key="n" :class="itemClasses[n]">
|
||||
<svg :viewBox="`0 0 ${textWidth} 10`" xmlns="http://www.w3.org/2000/svg">
|
||||
<path :d="pathData" />
|
||||
</svg>
|
||||
|
||||
@@ -22,12 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://leafus.net/pawkey" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://pawkey.dev" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
</div>
|
||||
<div v-if="$i != null" style="text-align: center;">
|
||||
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Pawkey</MkButton>
|
||||
</div>
|
||||
<FormSection v-if="instance.repositoryUrl !== 'https://git.leafus.net/pawkey/pawkey'">
|
||||
<FormSection v-if="instance.repositoryUrl !== 'https://git.pawlickers.org/pawkey/pawkey'">
|
||||
<div class="_gaps_s">
|
||||
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<div class="_gaps_s">
|
||||
<FormLink to="https://git.leafus.net/pawkey/pawkey" external>
|
||||
<FormLink to="https://git.pawlickers.org/pawkey/pawkey" external>
|
||||
<template #icon><i class="ph-code ph-bold ph-lg"></i></template>
|
||||
{{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original_pawkey }})
|
||||
<template #suffix>GitLab</template>
|
||||
@@ -128,81 +128,12 @@ const everyone = ref<Section[]>([
|
||||
heading: 'Pawkey Creators',
|
||||
people: fisher_yates([
|
||||
{
|
||||
handle: '@Leafus',
|
||||
avatar: 'https://git.leafus.net/uploads/-/system/user/avatar/2/avatar.png?width=192',
|
||||
link: 'https://leafus.net',
|
||||
handle: '@pawinput',
|
||||
avatar: 'https://git.pawlickers.org/avatars/5e909e3e9e43b099242596d67debcd61264809b7765f160b01c8a376f23e47a5?size=512',
|
||||
link: 'https://pawlickers.org',
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
heading: 'Sharkey Contributors',
|
||||
link: {
|
||||
label: i18n.ts._aboutMisskey.allContributors,
|
||||
url: 'https://activitypub.software/TransFem-org/Sharkey/-/graphs/develop',
|
||||
},
|
||||
people: fisher_yates([
|
||||
{
|
||||
handle: '@CenTdemeern1',
|
||||
avatar: 'https://secure.gravatar.com/avatar/e97dd57d32caf703cea556ace6304617b7420f17f5b1aac4a1eea8e4234735bb?s=128&d=identicon',
|
||||
link: 'https://activitypub.software/CenTdemeern1',
|
||||
},
|
||||
{
|
||||
handle: '@dakkar',
|
||||
avatar: 'https://secure.gravatar.com/avatar/c71b315eed7c63ff94c42b1b3e8dbad1?s=128&d=identicon',
|
||||
link: 'https://activitypub.software/dakkar',
|
||||
},
|
||||
{
|
||||
handle: '@hazelnoot',
|
||||
avatar: 'https://activitypub.software/uploads/-/system/user/avatar/5/avatar.png?width=128',
|
||||
link: 'https://activitypub.software/fEmber',
|
||||
},
|
||||
{
|
||||
handle: '@julia',
|
||||
avatar: 'https://activitypub.software/uploads/-/system/user/avatar/41/avatar.png?width=128',
|
||||
link: 'https://activitypub.software/julia',
|
||||
},
|
||||
{
|
||||
handle: '@Luna',
|
||||
avatar: 'https://secure.gravatar.com/avatar/4faf37df86a3d93a6c19ed6abf8588eade4efb837410dbbc53021b4fd12eaae7?s=128&d=identicon',
|
||||
link: 'https://activitypub.software/luna',
|
||||
},
|
||||
{
|
||||
handle: '@Marie',
|
||||
avatar: 'https://activitypub.software/uploads/-/system/user/avatar/2/avatar.png?width=128',
|
||||
link: 'https://activitypub.software/marie',
|
||||
},
|
||||
{
|
||||
handle: '@supakaity',
|
||||
avatar: 'https://activitypub.software/uploads/-/system/user/avatar/65/avatar.png?width=128',
|
||||
link: 'https://activitypub.software/supakaity',
|
||||
},
|
||||
{
|
||||
handle: '@tess',
|
||||
avatar: 'https://activitypub.software/uploads/-/system/user/avatar/132/avatar.png?width=128',
|
||||
link: 'https://activitypub.software/tess',
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
heading: 'Sharkey Testers',
|
||||
people: [
|
||||
{
|
||||
handle: '@lucent',
|
||||
avatar: 'https://antani.cyou/proxy/avatar.webp?url=https%3A%2F%2Fantani.cyou%2Ffiles%2Fa2944119-024c-4abd-86e5-64bf0d30b26f&avatar=1',
|
||||
link: 'https://antani.cyou/@lucent',
|
||||
},
|
||||
{
|
||||
handle: '@privateger',
|
||||
avatar: 'https://mediaproxy.plasmatrap.com/?url=https%3A%2F%2Fplasmatrap.com%2Ffiles%2F2cf35a8f-6520-4d4c-9611-bf22ee983293&avatar=1',
|
||||
link: 'https://plasmatrap.com/@privateger',
|
||||
},
|
||||
{
|
||||
handle: '@phoenix_fairy',
|
||||
avatar: 'https://thetransagenda.gay/proxy/avatar.webp?url=https%3A%2F%2Fs3.us-east-005.backblazeb2.com%2Ftranssharkey%2Fnull%2Fd93ac6dc-2020-4b5a-bce7-84b41e97a0ac.png&avatar=1',
|
||||
link: 'https://thetransagenda.gay/@phoenix_fairy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: i18n.ts._aboutMisskey.misskeyContributors,
|
||||
people: [
|
||||
|
||||
Reference in New Issue
Block a user