Merge branch 'develop' of codeberg.org:thatonecalculator/calckey into develop

This commit is contained in:
ThatOneCalculator 2022-12-03 16:02:12 -08:00
commit 422a66a242
18 changed files with 173 additions and 102 deletions

View File

@ -89,6 +89,8 @@
- Patron list - Patron list
- Animations respect reduced motion - Animations respect reduced motion
- Obliteration of Ai-chan - Obliteration of Ai-chan
- Undo renote button inside original note
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996) - [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021) - [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)

View File

@ -96,8 +96,9 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
## 💅 Customize ## 💅 Customize
- To add custom CSS for all users, edit `./custom/instance.css`. - To add custom CSS for all users, edit `./custom/assets/instance.css`.
- To add static assets (such as images for the splash screen), place them in the `./custom/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`. - To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
- To update custom assets without rebuilding, just run `yarn run gulp`. - To update custom assets without rebuilding, just run `yarn run gulp`.
## 🧑‍🔬 Configuring a new instance ## 🧑‍🔬 Configuring a new instance

0
custom/locales/.gitkeep Normal file
View File

View File

@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () =>
); );
gulp.task('copy:backend:custom', () => gulp.task('copy:backend:custom', () =>
gulp.src('./custom/*').pipe(gulp.dest('./packages/backend/assets/')) gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/'))
); );
gulp.task('copy:client:fonts', () => gulp.task('copy:client:fonts', () =>

View File

@ -4,6 +4,8 @@
const fs = require('fs'); const fs = require('fs');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
let languages = []
let languages_custom = []
const merge = (...args) => args.reduce((a, c) => ({ const merge = (...args) => args.reduce((a, c) => ({
...a, ...a,
@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
}), {}); }), {});
const languages = [
'ar-SA', fs.readdirSync(__dirname).forEach((file) => {
'cs-CZ', if (file.includes('.yml')){
'da-DK', file = file.slice(0, file.indexOf('.'))
'de-DE', languages.push(file);
'en-US', }
'es-ES', })
'fr-FR',
'id-ID', fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
'it-IT', if (file.includes('.yml')){
'ja-JP', file = file.slice(0, file.indexOf('.'))
'ja-KS', languages_custom.push(file);
'kab-KAB', }
'kn-IN', })
'ko-KR',
'nl-NL',
'no-NO',
'pl-PL',
'pt-PT',
'ru-RU',
'sk-SK',
'ug-CN',
'uk-UA',
'vi-VN',
'zh-CN',
'zh-TW',
];
const primaries = { const primaries = {
'en': 'US', 'en': 'US',
@ -51,6 +40,8 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {});
Object.assign(locales, locales_custom)
module.exports = Object.entries(locales) module.exports = Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => { .reduce((a, [k ,v]) => (a[k] = (() => {

View File

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "12.119.0-calc.18-rc.6", "version": "12.119.0-calc.18-rc.11",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<st
await updatePerson(actor.uri!, resolver, object); await updatePerson(actor.uri!, resolver, object);
return `ok: Person updated`; return `ok: Person updated`;
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await updateQuestion(object).catch(e => console.log(e)); await updateQuestion(object, resolver).catch(e => console.log(e));
return `ok: Question updated`; return `ok: Question updated`;
} else { } else {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;

View File

@ -271,7 +271,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
}); });
//#endregion //#endregion
await updateFeatured(user!.id).catch(err => logger.error(err)); await updateFeatured(user!.id, resolver).catch(err => logger.error(err));
return user!; return user!;
} }
@ -384,7 +384,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
}); });
await updateFeatured(exist.id).catch(err => logger.error(err)); await updateFeatured(exist.id, resolver).catch(err => logger.error(err));
} }
/** /**
@ -462,14 +462,14 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
return { fields, services }; return { fields, services };
} }
export async function updateFeatured(userId: User['id']) { export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await Users.findOneByOrFail({ id: userId }); const user = await Users.findOneByOrFail({ id: userId });
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
logger.info(`Updating the featured: ${user.uri}`); logger.info(`Updating the featured: ${user.uri}`);
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);

View File

@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @param uri URI of AP Question object * @param uri URI of AP Question object
* @returns true if updated * @returns true if updated
*/ */
export async function updateQuestion(value: any) { export async function updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -55,7 +55,7 @@ export async function updateQuestion(value: any) {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);

View File

@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
private user?: ILocalUser; private user?: ILocalUser;
private recursionLimit?: number;
constructor() { constructor(recursionLimit = 100) {
this.history = new Set(); this.history = new Set();
this.recursionLimit = recursionLimit;
} }
public getHistory(): string[] { public getHistory(): string[] {
@ -59,7 +61,9 @@ export default class Resolver {
if (this.history.has(value)) { if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
if (this.recursionLimit && this.history.size > this.recursionLimit) {
throw new Error('hit recursion limit');
}
this.history.add(value); this.history.add(value);
const host = extractDbHost(value); const host = extractDbHost(value);

View File

@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
return user && await packFeed(user); return user && await packFeed(user);
}; };
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
const reUser = new RegExp(`^/@(?<user>[^/]+?)(?:\.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$`);
router.get(reUser, async (ctx, next) => {
const groups = reUser.exec(ctx.originalUrl)?.groups;
if (!groups) {
await next();
return;
}
ctx.params = groups;
console.log(ctx, ctx.params)
if (groups.feed) {
if (groups.sub) {
await next();
return;
}
switch (groups.feed) {
case 'json':
await jsonFeed(ctx, next);
break;
case 'rss':
await rssFeed(ctx, next);
break;
case 'atom':
await atomFeed(ctx, next);
break;
}
return;
}
await userPage(ctx, next);
});
// Atom // Atom
router.get('/@:user.atom', async ctx => { const atomFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
// RSS // RSS
router.get('/@:user.rss', async ctx => { const rssFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
// JSON // JSON
router.get('/@:user.json', async ctx => { const jsonFeed: Router.Middleware = async ctx => {
const feed = await getFeed(ctx.params.user); const feed = await getFeed(ctx.params.user);
if (feed) { if (feed) {
@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => {
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); };
//#region SSR (for crawlers) //#region SSR (for crawlers)
// User // User
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { const userPage: Router.Middleware = async (ctx, next) => {
const { username, host } = Acct.parse(ctx.params.user); const userParam = ctx.params.user;
const subParam = ctx.params.sub;
const { username, host } = Acct.parse(userParam);
const user = await Users.findOneBy({ const user = await Users.findOneBy({
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: host ?? IsNull(), host: host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });
if (user != null) { if (user === null) {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
const meta = await fetchMeta();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
.map(field => field.value)
: [];
await ctx.render('user', {
user, profile, me,
avatarUrl: await Users.getAvatarUrl(user),
sub: ctx.params.sub,
instanceName: meta.name || 'Calckey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
privateMode: meta.privateMode,
});
ctx.set('Cache-Control', 'public, max-age=15');
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
await next(); await next();
return;
} }
});
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
const meta = await fetchMeta();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
.map(field => field.value)
: [];
const userDetail = {
user, profile, me,
avatarUrl: await Users.getAvatarUrl(user),
sub: subParam,
instanceName: meta.name || 'Calckey',
icon: meta.iconUrl,
themeColor: meta.themeColor,
privateMode: meta.privateMode,
};
await ctx.render('user', userDetail);
ctx.set('Cache-Control', 'public, max-age=15');
};
router.get('/users/:user', async ctx => { router.get('/users/:user', async ctx => {
const user = await Users.findOneBy({ const user = await Users.findOneBy({

View File

@ -428,6 +428,10 @@ function readPromo() {
padding: 28px 32px 18px; padding: 28px 32px 18px;
cursor: pointer; cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
> .avatar { > .avatar {
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;

View File

@ -347,7 +347,10 @@ if (appearNote.replyId) {
> .reply-to-more { > .reply-to-more {
opacity: 0.7; opacity: 0.7;
cursor: pointer; cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
} }
> .renote { > .renote {
@ -546,6 +549,10 @@ if (appearNote.replyId) {
> .reply { > .reply {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
cursor: pointer; cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
} }
> .reply, .reply-to, .reply-to-more { > .reply, .reply-to, .reply-to-more {

View File

@ -88,6 +88,10 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
flex: 1; flex: 1;
min-width: 0; min-width: 0;
cursor: pointer; cursor: pointer;
@media (pointer: coarse) {
cursor: default;
}
> .header { > .header {
margin-bottom: 2px; margin-bottom: 2px;

View File

@ -53,42 +53,62 @@ useTooltip(buttonRef, async (showing) => {
}, {}, 'closed'); }, {}, 'closed');
}); });
const renote = (viaKeyboard = false, ev?: MouseEvent) => { const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
pleaseLogin(); pleaseLogin();
if (defaultStore.state.seperateRenoteQuote) {
os.api('notes/create', { const renotes = await os.api('notes/renotes', {
renoteId: props.note.id, noteId: props.note.id,
visibility: props.note.visibility, limit: 11,
}); });
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) { const users = renotes.map(x => x.user.id);
const rect = el.getBoundingClientRect(); const hasRenotedBefore = users.includes($i.id);
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); let buttonActions = [{
os.popup(Ripple, { x, y }, {}, 'end'); text: i18n.ts.renote,
} icon: 'ph-repeat-bold ph-lg',
} else { danger: false,
os.popupMenu([{ action: () => {
text: i18n.ts.renote, os.api('notes/create', {
icon: 'ph-repeat-bold ph-lg', renoteId: props.note.id,
action: () => { visibility: props.note.visibility,
os.api('notes/create', { });
renoteId: props.note.id, const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
visibility: props.note.visibility, if (el) {
}); const rect = el.getBoundingClientRect();
}, const x = rect.left + (el.offsetWidth / 2);
}, { const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
}];
if (!defaultStore.state.seperateRenoteQuote) {
buttonActions.push({
text: i18n.ts.quote, text: i18n.ts.quote,
icon: 'ph-quotes-bold ph-lg', icon: 'ph-quotes-bold ph-lg',
danger: false,
action: () => { action: () => {
os.post({ os.post({
renote: props.note, renote: props.note,
}); });
}, },
}], buttonRef.value, {
viaKeyboard,
}); });
} }
if (hasRenotedBefore) {
buttonActions.push({
text: i18n.ts.unrenote,
icon: 'ph-trash-bold ph-lg',
danger: true,
action: () => {
os.api('notes/unrenote', {
noteId: props.note.id,
});
},
});
}
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
}; };
</script> </script>

View File

@ -90,7 +90,6 @@ function del(): void {
min-height: 38px; min-height: 38px;
border-radius: 16px; border-radius: 16px;
max-width: 100%; max-width: 100%;
margin-left: 4%;
& + * { & + * {
clear: both; clear: both;
@ -215,8 +214,6 @@ function del(): void {
> .balloon { > .balloon {
$color: var(--X4); $color: var(--X4);
margin-right: 4%;
margin-left: 0%;
background: $color; background: $color;
&.noText { &.noText {

View File

@ -4,6 +4,8 @@
"@shoq@newsroom.social", "@shoq@newsroom.social",
"@pikadude@erisly.social", "@pikadude@erisly.social",
"@sage@stop.voring.me", "@sage@stop.voring.me",
"@sky@therian.club" "@sky@therian.club",
"@panos@electricrequiem.com",
"@redhunt07@www.foxyhole.io"
] ]
} }