From 65cd605b739ae0d213b3502308e9cd523d3e1ae7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 21 Jan 2023 13:14:55 +0900 Subject: Achievements (#9665) * wip * Update ja-JP.yml * wip * wip * Update MkAchievements.vue * wip * :art: * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --- packages/backend/src/server/api/EndpointsModule.ts | 8 ++++++ packages/backend/src/server/api/endpoints.ts | 4 +++ .../server/api/endpoints/drive/folders/update.ts | 4 +-- packages/backend/src/server/api/endpoints/i.ts | 27 ++++++++++++++++--- .../server/api/endpoints/i/claim-achievement.ts | 28 +++++++++++++++++++ .../src/server/api/endpoints/users/achievements.ts | 31 ++++++++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/i/claim-achievement.ts create mode 100644 packages/backend/src/server/api/endpoints/users/achievements.ts (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 14927da7d6..466651f379 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -175,6 +175,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; @@ -329,6 +330,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___retention from './endpoints/retention.js'; import { GetterService } from './GetterService.js'; @@ -509,6 +511,7 @@ const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: e const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default }; const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default }; const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default }; +const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default }; const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default }; const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default }; const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default }; @@ -663,6 +666,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by- const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; +const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; @@ -847,6 +851,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_unregister, $i_apps, $i_authorizedApps, + $i_claimAchievement, $i_changePassword, $i_deleteAccount, $i_exportBlocking, @@ -1001,6 +1006,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, + $users_achievements, $fetchRss, $retention, ], @@ -1179,6 +1185,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_2fa_unregister, $i_apps, $i_authorizedApps, + $i_claimAchievement, $i_changePassword, $i_deleteAccount, $i_exportBlocking, @@ -1331,6 +1338,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_search, $users_show, $users_stats, + $users_achievements, $fetchRss, $retention, ], diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 54c4206ea4..3678fe14e8 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -174,6 +174,7 @@ import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js'; import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js'; import * as ep___i_apps from './endpoints/i/apps.js'; import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js'; +import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js'; import * as ep___i_changePassword from './endpoints/i/change-password.js'; import * as ep___i_deleteAccount from './endpoints/i/delete-account.js'; import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js'; @@ -328,6 +329,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___retention from './endpoints/retention.js'; @@ -506,6 +508,7 @@ const eps = [ ['i/2fa/unregister', ep___i_2fa_unregister], ['i/apps', ep___i_apps], ['i/authorized-apps', ep___i_authorizedApps], + ['i/claim-achievement', ep___i_claimAchievement], ['i/change-password', ep___i_changePassword], ['i/delete-account', ep___i_deleteAccount], ['i/export-blocking', ep___i_exportBlocking], @@ -660,6 +663,7 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], + ['users/achievements', ep___users_achievements], ['fetch-rss', ep___fetchRss], ['retention', ep___retention], ]; diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index ee63d291b2..ff0a78b929 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -28,8 +28,8 @@ export const meta = { recursiveNesting: { message: 'It can not be structured like nesting folders recursively.', - code: 'NO_SUCH_PARENT_FOLDER', - id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1', + code: 'RECURSIVE_NESTING', + id: 'dbeb024837894013aed44279f9199740', }, }, diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 3bcd6ff8fb..6beef5ab85 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -29,15 +29,36 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, user, token) => { const isSecure = token == null; - // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す - return await this.userEntityService.pack(user.id, user, { + const now = new Date(); + const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; + + // 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得 + const userProfile = await this.userProfilesRepository.findOneOrFail({ + where: { + userId: user.id, + }, + relations: ['user'], + }); + + if (!userProfile.loggedInDates.includes(today)) { + this.userProfilesRepository.update({ userId: user.id }, { + loggedInDates: [...userProfile.loggedInDates, today], + }); + userProfile.loggedInDates = [...userProfile.loggedInDates, today]; + } + + return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { detail: true, includeSecrets: isSecure, + userProfile, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts new file mode 100644 index 0000000000..52ae5475b6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -0,0 +1,28 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { AchievementService } from '@/core/AchievementService.js'; + +export const meta = { + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + private achievementService: AchievementService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.achievementService.create(me.id, ps.name); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts new file mode 100644 index 0000000000..2a095d83ea --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -0,0 +1,31 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserProfilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + + return profile.achievements; + }); + } +} -- cgit v1.2.3-freya From 8631740ca44b0e03d67ac2dd3c21ecf9ca9dcaa4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 21 Jan 2023 17:01:02 +0900 Subject: fix(server): twitterと連携するときに autwh is not a function になるのを修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #9658 --- CHANGELOG.md | 1 + packages/backend/src/server/api/integration/TwitterServerService.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index 26632b8399..85246595ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ You should also include the user name that made the change. ### Bugfixes - playを削除する手段がなかったのを修正 - The … button on notes does nothing when not logged in +- twitterと連携するときに autwh is not a function になるのを修正 ## 13.0.0 (2023/01/16) diff --git a/packages/backend/src/server/api/integration/TwitterServerService.ts b/packages/backend/src/server/api/integration/TwitterServerService.ts index 9cfadbfa1a..f31a788d31 100644 --- a/packages/backend/src/server/api/integration/TwitterServerService.ts +++ b/packages/backend/src/server/api/integration/TwitterServerService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { v4 as uuid } from 'uuid'; import { IsNull } from 'typeorm'; -import autwh from 'autwh'; +import * as autwh from 'autwh'; import type { Config } from '@/config.js'; import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -- cgit v1.2.3-freya From 3e112da486e59d48c415a5bd3a251148ed7312b3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 21 Jan 2023 20:40:09 +0900 Subject: ローカルのカスタム絵文字については直接オリジナルURLにリクエストするように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/entities/EmojiEntityService.ts | 6 ++++-- packages/backend/src/models/schema/emoji.ts | 4 ++++ packages/backend/src/server/api/endpoints/emojis.ts | 1 + packages/frontend/src/components/global/MkEmoji.vue | 6 +++++- packages/frontend/src/custom-emojis.ts | 9 +++++++-- 5 files changed, 21 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 2a4e09519f..cee85a5688 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -22,7 +22,7 @@ export class EmojiEntityService { @bindThis public async pack( src: Emoji['id'] | Emoji, - opts: { omitHost?: boolean; omitId?: boolean; } = {}, + opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {}, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); @@ -32,13 +32,15 @@ export class EmojiEntityService { name: emoji.name, category: emoji.category, host: opts.omitHost ? undefined : emoji.host, + // ?? emoji.originalUrl してるのは後方互換性のため + url: opts.withUrl ? (emoji.publicUrl ?? emoji.originalUrl) : undefined, }; } @bindThis public packMany( emojis: any[], - opts: { omitHost?: boolean; omitId?: boolean; } = {}, + opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {}, ) { return Promise.all(emojis.map(x => this.pack(x, opts))); } diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index d897a0fc05..143f25373c 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -29,5 +29,9 @@ export const packedEmojiSchema = { optional: true, nullable: true, description: 'The local host is represented with `null`.', }, + url: { + type: 'string', + optional: true, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 97dcfde596..db1eddc80a 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -83,6 +83,7 @@ export default class extends Endpoint { emojis: await this.emojiEntityService.packMany(emojis, { omitId: true, omitHost: true, + withUrl: true, }), }; }); diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index b7dd0296cd..aaad81c656 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -12,6 +12,7 @@ import { getStaticImageUrl } from '@/scripts/media-proxy'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; import { defaultStore } from '@/store'; import { getEmojiName } from '@/scripts/emojilist'; +import { customEmojis } from '@/custom-emojis'; const props = defineProps<{ emoji: string; @@ -30,6 +31,9 @@ const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'nati const url = computed(() => { if (char.value) { return char2path(char.value); + } else if (props.host == null) { + const found = customEmojis.find(x => x.name === customEmojiName); + return found ? found.url : null; } else { const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`; return defaultStore.state.disableShowingAnimatedImages @@ -38,7 +42,7 @@ const url = computed(() => { } }); const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value); -let errored = $ref(false); +let errored = $ref(isCustom.value && url.value == null); // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter function computeTitle(event: PointerEvent): void { diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 19469999b6..637ee9c06e 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -2,14 +2,19 @@ import { api } from './os'; import { miLocalStorage } from './local-storage'; const storageCache = miLocalStorage.getItem('emojis'); -export let customEmojis = storageCache ? JSON.parse(storageCache) : []; +export let customEmojis: { + name: string; + aliases: string[]; + category: string; + url: string; +}[] = storageCache ? JSON.parse(storageCache) : []; fetchCustomEmojis(); export async function fetchCustomEmojis() { const now = Date.now(); const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt'); - if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return; + if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60 * 24) return; const res = await api('emojis', {}); -- cgit v1.2.3-freya From 26ae2dfc0f494c377abd878c00044049fcd2bf37 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 22 Jan 2023 08:00:42 +0900 Subject: add achievement --- locales/ja-JP.yml | 3 +++ packages/backend/src/core/AchievementService.ts | 3 ++- packages/backend/src/server/api/endpoints/notes/favorites/create.ts | 6 ++++++ packages/frontend/src/scripts/achievements.ts | 6 ++++++ 4 files changed, 17 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 898ae01e72..57296b9857 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1049,6 +1049,9 @@ _achievements: _noteFavorited1: title: "星をみるひと" description: "初めてノートをお気に入りに登録した" + _myNoteFavorited1: + title: "星が欲しい" + description: "自分のノートが他の人からお気に入りに登録された" _profileFilled: title: "準備万端" description: "プロフィール設定を行った" diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 26dd356d36..be763e4629 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -44,6 +44,7 @@ const ACHIEVEMENT_TYPES = [ 'loggedInOnNewYearsDay', 'noteClipped1', 'noteFavorited1', + 'myNoteFavorited1', 'profileFilled', 'markedAsCat', 'following1', @@ -94,7 +95,7 @@ export class AchievementService { @bindThis public async create( userId: User['id'], - type: string, + type: typeof ACHIEVEMENT_TYPES[number], ): Promise { if (!ACHIEVEMENT_TYPES.includes(type)) return; diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index acf22a5ad4..e423f0f109 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -6,6 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import { AchievementService } from '@/core/AchievementService.js'; export const meta = { tags: ['notes', 'favorites'], @@ -51,6 +52,7 @@ export default class extends Endpoint { private idService: IdService, private getterService: GetterService, + private achievementService: AchievementService, ) { super(meta, paramDef, async (ps, me) => { // Get favoritee @@ -76,6 +78,10 @@ export default class extends Endpoint { noteId: note.id, userId: me.id, }); + + if (note.userHost == null) { + this.achievementService.create(note.userId, 'myNoteFavorited1'); + } }); } } diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index c97358e880..f511fce3ea 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -40,6 +40,7 @@ export const ACHIEVEMENT_TYPES = [ 'loggedInOnNewYearsDay', 'noteClipped1', 'noteFavorited1', + 'myNoteFavorited1', 'profileFilled', 'markedAsCat', 'following1', @@ -240,6 +241,11 @@ export const ACHIEVEMENT_BADGES = { bg: null, frame: 'bronze', }, + 'myNoteFavorited1': { + img: '/fluent-emoji/1f320.png', + bg: null, + frame: 'silver', + }, 'profileFilled': { img: '/fluent-emoji/1f44c.png', bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', -- cgit v1.2.3-freya