diff --git a/CALCKEY.md b/CALCKEY.md index 976cad22b2..dfcb191e41 100644 --- a/CALCKEY.md +++ b/CALCKEY.md @@ -89,6 +89,8 @@ - Patron list - Animations respect reduced motion - 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) - [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) diff --git a/README.md b/README.md index cb558cf686..8e4b03be94 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,9 @@ psql postgres -c "create database calckey with encoding = 'UTF8';" ## 💅 Customize -- To add custom CSS for all users, edit `./custom/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 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/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`. ## 🧑‍🔬 Configuring a new instance diff --git a/custom/instance.css b/custom/assets/instance.css similarity index 100% rename from custom/instance.css rename to custom/assets/instance.css diff --git a/custom/locales/.gitkeep b/custom/locales/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gulpfile.js b/gulpfile.js index 86f860e568..89a6acb839 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () => ); 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', () => diff --git a/locales/index.js b/locales/index.js index 92cd9b467c..7399bb5a18 100644 --- a/locales/index.js +++ b/locales/index.js @@ -4,6 +4,8 @@ const fs = require('fs'); const yaml = require('js-yaml'); +let languages = [] +let languages_custom = [] const merge = (...args) => args.reduce((a, c) => ({ ...a, @@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({ .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) }), {}); -const languages = [ - 'ar-SA', - 'cs-CZ', - 'da-DK', - 'de-DE', - 'en-US', - 'es-ES', - 'fr-FR', - 'id-ID', - 'it-IT', - 'ja-JP', - 'ja-KS', - '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', -]; + +fs.readdirSync(__dirname).forEach((file) => { + if (file.includes('.yml')){ + file = file.slice(0, file.indexOf('.')) + languages.push(file); + } +}) + +fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => { + if (file.includes('.yml')){ + file = file.slice(0, file.indexOf('.')) + languages_custom.push(file); + } +}) const primaries = { 'en': 'US', @@ -51,6 +40,8 @@ const primaries = { 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_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) .reduce((a, [k ,v]) => (a[k] = (() => { diff --git a/package.json b/package.json index 05510cafb8..d732f2f79f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "calckey", - "version": "12.119.0-calc.18-rc.6", + "version": "12.119.0-calc.18-rc.11", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index 9e8a81bb39..022be0ad8e 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise console.log(e)); + await updateQuestion(object, resolver).catch(e => console.log(e)); return `ok: Question updated`; } else { return `skip: Unknown type: ${getApType(object)}`; diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 6097e3b6ed..5ef04588e9 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -271,7 +271,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise logger.error(err)); + await updateFeatured(user!.id, resolver).catch(err => logger.error(err)); 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), }); - 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 }; } -export async function updateFeatured(userId: User['id']) { +export async function updateFeatured(userId: User['id'], resolver?: Resolver) { const user = await Users.findOneByOrFail({ id: userId }); if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); - const resolver = new Resolver(); + if (resolver == null) resolver = new Resolver(); // Resolve to (Ordered)Collection Object const collection = await resolver.resolveCollection(user.featured); diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts index f0321fdf2f..94a50d4f71 100644 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ b/packages/backend/src/remote/activitypub/models/question.ts @@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver * @param uri URI of AP Question object * @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; // URIがこのサーバーを指しているならスキップ @@ -55,7 +55,7 @@ export async function updateQuestion(value: any) { //#endregion // resolve new Question object - const resolver = new Resolver(); + if (resolver == null) resolver = new Resolver(); const question = await resolver.resolve(value) as IQuestion; apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 5c9d44292e..94227e4db5 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js'; export default class Resolver { private history: Set; private user?: ILocalUser; + private recursionLimit?: number; - constructor() { + constructor(recursionLimit = 100) { this.history = new Set(); + this.recursionLimit = recursionLimit; } public getHistory(): string[] { @@ -59,7 +61,9 @@ export default class Resolver { if (this.history.has(value)) { 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); const host = extractDbHost(value); diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index c81506384c..446df15541 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -232,8 +232,43 @@ const getFeed = async (acct: string) => { 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(`^/@(?[^/]+?)(?:\.(?json|rss|atom))?(?:/(?[^/]+))?$`); +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 -router.get('/@:user.atom', async ctx => { +const atomFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => { } else { ctx.status = 404; } -}); +}; // RSS -router.get('/@:user.rss', async ctx => { +const rssFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => { } else { ctx.status = 404; } -}); +}; // JSON -router.get('/@:user.json', async ctx => { +const jsonFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => { } else { ctx.status = 404; } -}); +}; //#region SSR (for crawlers) // User -router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); +const userPage: Router.Middleware = async (ctx, next) => { + const userParam = ctx.params.user; + const subParam = ctx.params.sub; + const { username, host } = Acct.parse(userParam); + const user = await Users.findOneBy({ usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, }); - 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にはしない + if (user === null) { 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 => { const user = await Users.findOneBy({ diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 20d2bbee47..613203091b 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -428,6 +428,10 @@ function readPromo() { padding: 28px 32px 18px; cursor: pointer; + @media (pointer: coarse) { + cursor: default; + } + > .avatar { flex-shrink: 0; display: block; diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue index 54d7ec0ca9..2deb184416 100644 --- a/packages/client/src/components/MkNoteDetailed.vue +++ b/packages/client/src/components/MkNoteDetailed.vue @@ -347,7 +347,10 @@ if (appearNote.replyId) { > .reply-to-more { opacity: 0.7; cursor: pointer; - + + @media (pointer: coarse) { + cursor: default; + } } > .renote { @@ -546,6 +549,10 @@ if (appearNote.replyId) { > .reply { border-top: solid 0.5px var(--divider); cursor: pointer; + + @media (pointer: coarse) { + cursor: default; + } } > .reply, .reply-to, .reply-to-more { diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index c1943920df..5a3539222c 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -88,6 +88,10 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item flex: 1; min-width: 0; cursor: pointer; + + @media (pointer: coarse) { + cursor: default; + } > .header { margin-bottom: 2px; diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index c3cc5794e8..ffba924f95 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -53,42 +53,62 @@ useTooltip(buttonRef, async (showing) => { }, {}, 'closed'); }); -const renote = (viaKeyboard = false, ev?: MouseEvent) => { +const renote = async (viaKeyboard = false, ev?: MouseEvent) => { pleaseLogin(); - if (defaultStore.state.seperateRenoteQuote) { - os.api('notes/create', { - renoteId: props.note.id, - visibility: props.note.visibility, - }); - const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; - 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'); - } - } else { - os.popupMenu([{ - text: i18n.ts.renote, - icon: 'ph-repeat-bold ph-lg', - action: () => { - os.api('notes/create', { - renoteId: props.note.id, - visibility: props.note.visibility, - }); - }, - }, { + + const renotes = await os.api('notes/renotes', { + noteId: props.note.id, + limit: 11, + }); + + const users = renotes.map(x => x.user.id); + const hasRenotedBefore = users.includes($i.id); + + let buttonActions = [{ + text: i18n.ts.renote, + icon: 'ph-repeat-bold ph-lg', + danger: false, + action: () => { + os.api('notes/create', { + renoteId: props.note.id, + visibility: props.note.visibility, + }); + const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; + 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, icon: 'ph-quotes-bold ph-lg', + danger: false, action: () => { os.post({ 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 }); }; diff --git a/packages/client/src/pages/messaging/messaging-room.message.vue b/packages/client/src/pages/messaging/messaging-room.message.vue index 9e2af50466..7e66dfcc2d 100644 --- a/packages/client/src/pages/messaging/messaging-room.message.vue +++ b/packages/client/src/pages/messaging/messaging-room.message.vue @@ -90,7 +90,6 @@ function del(): void { min-height: 38px; border-radius: 16px; max-width: 100%; - margin-left: 4%; & + * { clear: both; @@ -215,8 +214,6 @@ function del(): void { > .balloon { $color: var(--X4); - margin-right: 4%; - margin-left: 0%; background: $color; &.noText { diff --git a/patrons.json b/patrons.json index 65306d72f8..194c0e98c3 100644 --- a/patrons.json +++ b/patrons.json @@ -4,6 +4,8 @@ "@shoq@newsroom.social", "@pikadude@erisly.social", "@sage@stop.voring.me", - "@sky@therian.club" + "@sky@therian.club", + "@panos@electricrequiem.com", + "@redhunt07@www.foxyhole.io" ] }