Achievements 1.5/2

This commit is contained in:
Crimekillz 2024-04-06 19:13:08 +02:00
parent 30ad1c47f0
commit d2a86713af
17 changed files with 425 additions and 237 deletions

View File

@ -2,7 +2,7 @@ version: "3"
services:
web:
image: iceshrimp.dev/Crimekillz/trashposs:dev
image: iceshrimp.dev/crimekillz/trashposs:dev
### If you want to build the image locally
# build: .
### If you want to build the image locally AND use Docker 20.10

View File

@ -1140,218 +1140,218 @@ bitYou: "chomped you"
bitYouBack: "chomped you back"
achievements: "Achievements"
_achievements:
earnedAt: "獲得日時"
earnedAt: "Earn date and time"
_types:
_notes1:
title: "Just set up my TrashPoss account"
description: "This is my first post"
flavor: "Screm into the void!"
_notes10:
title: "いくつかのノート"
description: "ートを10回投稿した"
title: "Some Posts"
description: "Posted 10 times"
_notes100:
title: "たくさんのノート"
description: "ートを100回投稿した"
title: "Lots of Posts"
description: "Posted 100 times"
_notes500:
title: "ノートまみれ"
description: "ートを500回投稿した"
title: "Post Covered"
description: "Posted 500 times"
_notes1000:
title: "ノートの山"
description: "ートを1,000回投稿した"
title: "Pile of Posts"
description: "Posted 1,000 times"
_notes5000:
title: "湧き出るノート"
description: "ートを5,000回投稿した"
title: "Gushing Post"
description: "Posted 5,000 times"
_notes10000:
title: "スーパーノート"
description: "ートを10,000回投稿した"
title: "Super Post"
description: "Posted 10,000 times"
_notes20000:
title: "ニードモアノート"
description: "ートを20,000回投稿した"
title: "Need More Posts"
description: "Posted 20,000 times"
_notes30000:
title: "ノートノートノート"
description: "ートを30,000回投稿した"
title: "Post Post Post"
description: "Posted 30,000 times"
_notes40000:
title: "ノート工場"
description: "ートを40,000回投稿した"
title: "Post Factory"
description: "Posted 40,000 times"
_notes50000:
title: "ノートの惑星"
description: "ートを50,000回投稿した"
title: "Planet of Posts"
description: "Posted 50,000 times"
_notes60000:
title: "ノートクエーサー"
description: "ートを60,000回投稿した"
title: "Post Quasar"
description: "Posted 60,000 times"
_notes70000:
title: "ブラックノートホール"
description: "ートを70,000回投稿した"
title: "Black Post Hole"
description: "Posted 70,000 times"
_notes80000:
title: "ノートギャラクシー"
description: "ートを80,000回投稿した"
title: "Post Galaxy"
description: "Posted 80,000 times"
_notes90000:
title: "ノートバース"
description: "ートを90,000回投稿した"
title: "Postiverse"
description: "Posted 90,000 times"
_notes100000:
title: "ALL YOUR POSTS ARE BELONG TO US"
description: "ートを100,000回投稿した"
flavor: "そんなに書くことある?"
description: "Posted 100,000 times"
flavor: "Do you have much to write about?"
_login3:
title: "ビギナーⅠ"
description: "通算ログイン日数が3日"
flavor: "今日からね僕は ミスキストってことで"
title: "Beginner I"
description: "Total login days are 3 days"
flavor: "From today onwards, I think they're a Misquist."
_login7:
title: "ビギナーⅡ"
description: "通算ログイン日数が7日"
flavor: "慣れてきましたか?"
title: "Beginner II"
description: "Total login days are 7 days"
flavor: "Are you getting used to it?"
_login15:
title: "ビギナーⅢ"
description: "通算ログイン日数が15日"
title: "Beginner III"
description: "Total number of login days is 15"
_login30:
title: "ミスキストⅠ"
description: "通算ログイン日数が30日"
title: "Miscist I"
description: "Total login days are 30 days"
_login60:
title: "ミスキストⅡ"
description: "通算ログイン日数が60日"
title: "Miscist II"
description: "Total number of login days is 60"
_login100:
title: "ミスキストⅢ"
description: "通算ログイン日数が100日"
flavor: "そのユーザー、ミスキストにつき"
title: "Miscist III"
description: "Total number of login days is 100"
flavor: "For that user, Misquist"
_login200:
title: "常連Ⅰ"
description: "通算ログイン日数が200日"
title: "Regular I"
description: "Total number of login days is 200"
_login300:
title: "常連Ⅱ"
description: "通算ログイン日数が300日"
title: "Regular II"
description: "Total number of login days is 300"
_login400:
title: "常連Ⅲ"
description: "通算ログイン日数が400日"
title: "Regular III"
description: "Total number of login days is 400"
_login500:
title: "ベテランⅠ"
description: "通算ログイン日数が500日"
flavor: "諸君、私はノートが好きだ"
title: "Veteran I"
description: "Total number of login days is 500"
flavor: "Well, I like posts..."
_login600:
title: "ベテランⅡ"
description: "通算ログイン日数が600日"
title: "Veteran II"
description: "Total number of login days is 600"
_login700:
title: "ベテランⅢ"
description: "通算ログイン日数が700日"
title: "Veteran III"
description: "Total number of login days is 700"
_login800:
title: "ノートマスターⅠ"
description: "通算ログイン日数が800日"
title: "Post Master I"
description: "Total number of login days is 800"
_login900:
title: "ノートマスターⅡ"
description: "通算ログイン日数が900日"
title: "Post Master II"
description: "Total number of login days is 900"
_login1000:
title: "ノートマスターⅢ"
description: "通算ログイン日数が1,000日"
flavor: "Misskeyを使ってくれてありがとう"
title: "Post Master III"
description: "Total number of login days is 1,000"
flavor: "Thank you for using TrashPoss!"
_noteClipped1:
title: "クリップせずにはいられないな"
description: "初めてノートをクリップした"
title: "I can't help but clip"
description: "Clipped a post for the first time"
_noteFavorited1:
title: "星をみるひと"
description: "初めてノートをお気に入りに登録した"
title: "People who look at the stars"
description: "I registered a note as a favorite for the first time"
_profileFilled:
title: "準備万端"
description: "プロフィール設定を行った"
title: "Ready to go"
description: "Profile settings were made"
_markedAsCat:
title: "吾輩は猫である"
description: "アカウントをCatとして設定した"
flavor: "名前はまだない。"
title: "I am a kitty"
description: "Account set up in Cat-mode"
flavor: "Please name me :3"
_following1:
title: "はじめてのフォロー"
description: "初めてフォローした"
title: "First follow"
description: "First time following"
_following10:
title: "ついてく、ついてく"
description: "フォローが10人を超した"
title: "Follow me, follow me"
description: "More than 10 followers"
_following50:
title: "友達たくさん"
description: "フォローが50人を超した"
title: "Lots of friends"
description: "Following exceeded 50 people"
_following100:
title: "友達100人"
description: "フォローが100人を超した"
title: "100 Friends"
description: "Following exceeded 100 people"
_following300:
title: "友達過多"
description: "フォローが300人を超した"
title: "Too many friends"
description: "Over 300 followers"
_followers1:
title: "はじめてのフォロワー"
description: "初めてフォローされた"
title: "First Follower"
description: "First followed"
_followers10:
title: "フォローミー!"
description: "フォロワーが10人を超した"
title: "Follow me!"
description: "More than 10 followers"
_followers50:
title: "ぞろぞろ"
description: "フォロワーが50人を超した"
title: "Zorozoro"
description: "Over 50 followers"
_followers100:
title: "人気者"
description: "フォロワーが100人を超した"
title: "Popular"
description: "Over 100 followers"
_followers300:
title: "一列でお並びください"
description: "フォロワーが300人を超した"
title: "Please stand in line"
description: "Over 300 followers"
_followers500:
title: "基地局"
description: "フォロワーが500人を超した"
title: "Base Station"
description: "Over 500 followers"
_followers1000:
title: "インフルエンサー"
description: "フォロワーが1,000人を超した"
title: "Influencer"
description: "Over 1,000 followers"
_collectAchievements30:
title: "実績コレクター"
description: "実績を30個以上獲得した"
title: "Achievement Collector"
description: "Obtained 30 or more achievements"
_iLoveMisskey:
title: "I Love TrashPoss"
description: "\"I ❤ #TrashPoss\"を投稿した"
flavor: "Misskeyを使ってくださりありがとうございます by 開発チーム"
description: "I posted \"I ❤ #TrashPoss\""
flavor: "Thank you for using TrashPoss! by Development Team"
_client30min:
title: "ひとやすみ"
description: "クライアントを起動してから30分以上経過した"
title: "Take a break"
description: "More than 30 minutes have passed since the client was started"
_noteDeletedWithin1min:
title: "いまのなし"
description: "投稿してから1分以内にその投稿を削除した"
title: "Now Nothing"
description: "The post was deleted within 1 minute of posting"
_postedAtLateNight:
title: "夜行性"
description: "深夜にノートを投稿した"
flavor: "そろそろ寝よう。"
title: "Nocturnal"
description: "Posted a note late at night"
flavor: "Let's go to sleep."
_postedAt0min0sec:
title: "時報"
description: "0分0秒にートを投稿した"
flavor: "ポッ ポッ ポッ ピーン"
title: "Time signal"
description: "Posted a note at 0 minutes 0 seconds"
flavor: "Pop Pop Pop Peen"
_selfQuote:
title: "自己言及"
description: "自分のノートを引用した"
title: "Self-reference"
description: "Quoted from my own notes"
_htl20npm:
title: "流れるTL"
description: "ホームタイムラインの流速が20npmを越す"
title: "Flowing TL"
description: "Home timeline flow rate exceeds 20npm"
_driveFolderCircularReference:
title: "循環参照"
description: "ドライブのフォルダを再帰的な入れ子にしようとした"
title: "Circular Reference"
description: "Attempted to recursively nest drive folders"
_reactWithoutRead:
title: "ちゃんと読んだ?"
description: "100文字以上のテキストを含むートに投稿されてから3秒以内にリアクションした"
title: "Did you read it properly?"
description: "Reacted within 3 seconds of being posted to a note containing more than 100 characters of text."
_clickedClickHere:
title: "ここをクリック"
description: "ここをクリックした"
title: "Click here"
description: "Clicked here"
_justPlainLucky:
title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得"
title: "Just Lucky"
description: "Obtained every 10 seconds with a 0.01% chance"
_setNameToSyuilo:
title: "神様コンプレックス"
description: "名前を syuilo に設定した"
title: "Poss gang, Poss gang"
description: "Name set to Crimekillz"
_passedSinceAccountCreated1:
title: "一周年"
description: "アカウント作成から1年経過した"
title: "One Year Anniversary"
description: "One year has passed since account creation"
_passedSinceAccountCreated2:
title: "二周年"
description: "アカウント作成から2年経過した"
title: "Second Anniversary"
description: "2 years have passed since account creation"
_passedSinceAccountCreated3:
title: "三周年"
description: "アカウント作成から3年経過した"
title: "Third Anniversary"
description: "3 years have passed since account creation"
_loggedInOnBirthday:
title: "ハッピーバースデー"
description: "誕生日にログインした"
title: "Happy Birthday"
description: "Logged in on my birthday"
_cookieClicked:
title: "クッキーをクリックするゲーム"
description: "クッキーをクリックした"
flavor: "ソフト間違ってない?"
title: "Cookie Clicking Game"
description: "You clicked on the cookie"
flavor: "Isn't the software wrong?"
_brainDiver:
title: "Brain Diver"
description: "Brain Diverへのリンクを投稿した"
description: "Posted a link to Brain Diver"
flavor: "TrashPoss-TrashPoss La-Tu-Ma"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing

View File

@ -8,7 +8,7 @@ export class FederatedBite1705528046452 implements MigrationInterface {
await queryRunner.query(`CREATE TABLE "bite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "uri" character varying(512), "userId" character varying(32) NOT NULL, "targetType" "public"."bite_targettype_enum" NOT NULL, "targetUserId" character varying(32), "targetBiteId" character varying(32), "replied" boolean NOT NULL DEFAULT true, CONSTRAINT "CHK_c3a20c5756ccff3133f8927500" CHECK ("targetUserId" IS NOT NULL OR "targetBiteId" IS NOT NULL), CONSTRAINT "PK_1887f3f621a4a7655a1b78bfd66" PRIMARY KEY ("id")); COMMENT ON COLUMN "bite"."uri" IS 'null if local'`);
await queryRunner.query(`ALTER TABLE "notification" ADD "biteId" character varying(32)`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app', 'bite')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
@ -18,7 +18,7 @@ export class FederatedBite1705528046452 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9" FOREIGN KEY ("targetBiteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_c54844158c1eead7042e7ca4c83" FOREIGN KEY ("biteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app', 'bite')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
}
@ -28,7 +28,7 @@ export class FederatedBite1705528046452 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9"`);
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_a646fbbeb6efa2531c75fec46b9"`);
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_8d00aa79e157364ac1f60c15098"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
@ -37,7 +37,7 @@ export class FederatedBite1705528046452 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "biteId"`);
await queryRunner.query(`DROP TABLE "bite"`);
await queryRunner.query(`DROP TYPE "public"."bite_targettype_enum"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);

View File

@ -138,6 +138,11 @@ export class Notification {
})
public choice: number | null;
@Column('varchar', {
length: 128, nullable: true,
})
public achievement: string | null;
/**
* App notification body
*/

View File

@ -239,6 +239,19 @@ export class UserProfile {
})
public mutingNotificationTypes: typeof notificationTypes[number][];
@Column('varchar', {
length: 32, array: true, default: '{}',
})
public loggedInDates: string[];
@Column('jsonb', {
default: [],
})
public achievements: {
name: string;
unlockedAt: number;
}[];
//#region Denormalized fields
@Index()
@Column("varchar", {

View File

@ -149,6 +149,11 @@ export const NotificationRepository = db.getRepository(Notification).extend({
bite: notification.bite ?? await Bites.findOneBy({ id: notification.biteId! }),
}
: {}),
...(notification.type === "achievementEarned"
? {
achievement: notification.achievement,
}
: {}),
});
},

View File

@ -37,6 +37,7 @@ import {
} from "../index.js";
import type { Instance } from "../entities/instance.js";
import AsyncLock from "async-lock";
import { UserProfile } from "../entities/user-profile.js";
const userInstanceCache = new Cache<Instance | null>(
"userInstance",
@ -412,6 +413,7 @@ export const UserRepository = db.getRepository(User).extend({
detail?: D;
includeSecrets?: boolean;
isPrivateMode?: boolean;
userProfile?: UserProfile,
},
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign(
@ -447,9 +449,7 @@ export const UserRepository = db.getRepository(User).extend({
.orderBy("pin.id", "DESC")
.getMany()
: [];
const profile = opts.detail
? await UserProfiles.findOneByOrFail({ userId: user.id })
: null;
const profile = opts.detail ? (opts.userProfile ?? await UserProfiles.findOneByOrFail({ userId: user.id })) : null;
const followingCount =
profile == null
@ -625,6 +625,8 @@ export const UserRepository = db.getRepository(User).extend({
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
}
: {}),

View File

@ -81,5 +81,10 @@ export const packedNotificationSchema = {
optional: true,
nullable: true,
},
achievement: {
type: "object",
optional: true,
nullable: true,
},
},
} as const;

View File

@ -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";
@ -332,6 +333,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___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js";
import * as ep___bites_create from "./endpoints/bites/create.js";
@ -525,6 +527,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],
@ -681,6 +684,7 @@ const eps = [
["users/search", ep___users_search],
["users/show", ep___users_show],
["users/stats", ep___users_stats],
["users/achievements", ep___users_achievements],
["admin/drive-capacity-override", ep___admin_driveCapOverride],
["fetch-rss", ep___fetchRss],
["get-sounds", ep___sounds],

View File

@ -1,4 +1,4 @@
import { Users } from "@/models/index.js";
import { UserProfiles, Users } from "@/models/index.js";
import define from "../define.js";
export const meta = {
@ -23,9 +23,27 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null;
// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
return await Users.pack<true, true>(user.id, user, {
const now = new Date();
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
const userProfile = await UserProfiles.findOneOrFail({
where: {
userId: user.id,
},
relations: ['user'],
});
if (!userProfile.loggedInDates.includes(today)) {
UserProfiles.update({ userId: user.id }, {
loggedInDates: [...userProfile.loggedInDates, today],
});
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
}
return await Users.pack<true, true>(userProfile.user!, userProfile.user!, {
detail: true,
includeSecrets: isSecure,
userProfile,
});
});

View File

@ -0,0 +1,18 @@
import { createAchievement } from '@/services/achievement-service.js';
import define from "../../define.js";
export const meta = {
requireCredential: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string' },
},
required: ['name'],
} as const;
export default define(meta, paramDef, async (ps, me) => {
await createAchievement(me.id, ps.name);
});

View File

@ -0,0 +1,22 @@
import { UserProfiles } from '@/models/index.js';
import define from "../../define.js";
export const meta = {
tags: ["users", "achievements"],
requireCredential: true,
description: "Show all achievements this user made.",
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
export default define(meta, paramDef, async (ps, me) => {
const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId });
return profile.achievements;
});

View File

@ -0,0 +1,96 @@
import { UserProfiles, Users } from '@/models/index.js';
import type { User } from '@/models/entities/user.js';
import { createNotification } from '@/services/create-notification.js';
const ACHIEVEMENT_TYPES = [
'notes1',
'notes10',
'notes100',
'notes500',
'notes1000',
'notes5000',
'notes10000',
'notes20000',
'notes30000',
'notes40000',
'notes50000',
'notes60000',
'notes70000',
'notes80000',
'notes90000',
'notes100000',
'login3',
'login7',
'login15',
'login30',
'login60',
'login100',
'login200',
'login300',
'login400',
'login500',
'login600',
'login700',
'login800',
'login900',
'login1000',
'passedSinceAccountCreated1',
'passedSinceAccountCreated2',
'passedSinceAccountCreated3',
'loggedInOnBirthday',
'noteClipped1',
'noteFavorited1',
'profileFilled',
'markedAsCat',
'following1',
'following10',
'following50',
'following100',
'following300',
'followers1',
'followers10',
'followers50',
'followers100',
'followers300',
'followers500',
'followers1000',
'collectAchievements30',
'iLoveMisskey',
'client30min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',
'selfQuote',
'htl20npm',
'driveFolderCircularReference',
'reactWithoutRead',
'clickedClickHere',
'justPlainLucky',
'setNameToSyuilo',
'cookieClicked',
'brainDiver',
] as const;
export async function createAchievement(
userId: User['id'],
type: string,
) {
if (!ACHIEVEMENT_TYPES.includes(type)) return;
const date = Date.now();
const profile = await UserProfiles.findOneByOrFail({ userId: userId });
if (profile.achievements.some(a => a.name === type)) return;
await UserProfiles.update(userId, {
achievements: [...profile.achievements, {
name: type,
unlockedAt: date,
}],
});
createNotification(userId, 'achievementEarned', {
achievement: type,
});
}

View File

@ -1,34 +1,34 @@
<template>
<div>
<div v-if="achievements" :class="'.root'">
<div v-for="achievement in achievements" :key="achievement" :class="'.achievement'" class="_panel">
<div :class="'.icon'">
<div :class="['.iconFrame', ['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div :class="['.iconInner']" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="'.iconImg'" :src="ACHIEVEMENT_BADGES[achievement.name].img">
<div v-if="achievements" class="root">
<div v-for="achievement in achievements" :key="achievement" class="_panel achievement">
<div class="icon">
<div :class="[['iconFrame'], ['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div class="iconInner" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img class="iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div>
</div>
</div>
<div :class="'.body'">
<div :class="'.header'">
<span :class="'.title'">{{ i18n.ts._achievements._types['_' + achievement.name].title }}</span>
<span :class="'.time'">
<div class="body">
<div class="header">
<span class="title">{{ i18n.ts._achievements._types['_' + achievement.name].title }}</span>
<span class="time">
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
</span>
</div>
<div :class="'.description'">{{ i18n.ts._achievements._types['_' + achievement.name].description }}</div>
<div v-if="i18n.ts._achievements._types['_' + achievement.name].flavor" :class="'.flavor'">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
<div class="description">{{ i18n.ts._achievements._types['_' + achievement.name].description }}</div>
<div v-if="i18n.ts._achievements._types['_' + achievement.name].flavor" class="flavor">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
</div>
</div>
<template v-if="withLocked">
<div v-for="achievement in lockedAchievements" :key="achievement" :class="['.achievement', '.locked']" class="_panel" @click="achievement === 'clickedClickHere' ? clickHere() : () => {}">
<div :class="'.icon'">
<div v-for="achievement in lockedAchievements" :key="achievement" class="_panel achievement locked" @click="achievement === 'clickedClickHere' ? clickHere() : () => {}">
<div class="icon">
</div>
<div :class="'.body'">
<div :class="'.header'">
<span :class="'.title'">???</span>
<div class="body">
<div class="header">
<span class="title">???</span>
</div>
<div :class="'.description'">???</div>
<div class="description">???</div>
</div>
</div>
</template>
@ -77,7 +77,7 @@ onMounted(() => {
});
</script>
<style lang="scss" module>
<style lang="scss" scoped>
.root {
display: grid;
grid-template-columns: repeat(auto-fill, min(380px, 100%));

View File

@ -997,7 +997,7 @@ async function post() {
}
const text = postData.text?.toLowerCase() ?? '';
if ((text.includes('love') || text.includes('❤')) && text.includes('misskey')) {
if ((text.includes('love') || text.includes('❤')) && text.includes('trashposs')) {
claimAchievement('iLoveMisskey');
}
if (text.includes('Efrlqw8ytg4'.toLowerCase()) || text.includes('XVCwzwxdHuA'.toLowerCase())) {

View File

@ -245,7 +245,7 @@ function save() {
speakAsCat: !!profile.speakAsCat,
});
claimAchievement('profileFilled');
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
if (profile.name === 'Crimekillz' || profile.name === 'crimekillz') {
claimAchievement('setNameToSyuilo');
}
if (profile.isCat) {

View File

@ -72,332 +72,332 @@ export const ACHIEVEMENT_TYPES = [
export const ACHIEVEMENT_BADGES = {
'notes1': {
img: '/fluent-emoji/1f4dd.png',
img: '/twemoji/1f4dd.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes10': {
img: '/fluent-emoji/1f4d1.png',
img: '/twemoji/1f4d1.svg',
bg: null,
frame: 'bronze',
},
'notes100': {
img: '/fluent-emoji/1f4d2.png',
img: '/twemoji/1f4d2.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes500': {
img: '/fluent-emoji/1f4da.png',
img: '/twemoji/1f4da.svg',
bg: null,
frame: 'bronze',
},
'notes1000': {
img: '/fluent-emoji/1f5c3.png',
img: '/twemoji/1f5c3.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes5000': {
img: '/fluent-emoji/1f304.png',
img: '/twemoji/1f304.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'notes10000': {
img: '/fluent-emoji/1f3d9.png',
img: '/twemoji/1f3d9.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'silver',
},
'notes20000': {
img: '/fluent-emoji/1f307.png',
img: '/twemoji/1f307.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'silver',
},
'notes30000': {
img: '/fluent-emoji/1f306.png',
img: '/twemoji/1f306.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'notes40000': {
img: '/fluent-emoji/1f303.png',
img: '/twemoji/1f303.svg',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'silver',
},
'notes50000': {
img: '/fluent-emoji/1fa90.png',
img: '/twemoji/1fa90.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'gold',
},
'notes60000': {
img: '/fluent-emoji/2604.png',
img: '/twemoji/2604.svg',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'gold',
},
'notes70000': {
img: '/fluent-emoji/1f30c.png',
img: '/twemoji/1f30c.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'gold',
},
'notes80000': {
img: '/fluent-emoji/1f30c.png',
img: '/twemoji/1f30c.svg',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'gold',
},
'notes90000': {
img: '/fluent-emoji/1f30c.png',
img: '/twemoji/1f30c.svg',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'gold',
},
'notes100000': {
img: '/fluent-emoji/267e.png',
img: '/twemoji/267e.svg',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'platinum',
},
'login3': {
img: '/fluent-emoji/1f331.png',
img: '/twemoji/1f331.svg',
bg: null,
frame: 'bronze',
},
'login7': {
img: '/fluent-emoji/1f331.png',
img: '/twemoji/1f331.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'login15': {
img: '/fluent-emoji/1f331.png',
img: '/twemoji/1f331.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},
'login30': {
img: '/fluent-emoji/1fab4.png',
img: '/twemoji/1fab4.svg',
bg: null,
frame: 'bronze',
},
'login60': {
img: '/fluent-emoji/1fab4.png',
img: '/twemoji/1fab4.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'login100': {
img: '/fluent-emoji/1fab4.png',
img: '/twemoji/1fab4.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'silver',
},
'login200': {
img: '/fluent-emoji/1f333.png',
img: '/twemoji/1f333.svg',
bg: null,
frame: 'silver',
},
'login300': {
img: '/fluent-emoji/1f333.png',
img: '/twemoji/1f333.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'silver',
},
'login400': {
img: '/fluent-emoji/1f333.png',
img: '/twemoji/1f333.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'silver',
},
'login500': {
img: '/fluent-emoji/1f304.png',
img: '/twemoji/1f304.svg',
bg: null,
frame: 'silver',
},
'login600': {
img: '/fluent-emoji/1f304.png',
img: '/twemoji/1f304.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'gold',
},
'login700': {
img: '/fluent-emoji/1f304.png',
img: '/twemoji/1f304.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'gold',
},
'login800': {
img: '/fluent-emoji/1f307.png',
img: '/twemoji/1f307.svg',
bg: null,
frame: 'gold',
},
'login900': {
img: '/fluent-emoji/1f307.png',
img: '/twemoji/1f307.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'gold',
},
'login1000': {
img: '/fluent-emoji/1f307.png',
img: '/twemoji/1f307.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'platinum',
},
'noteClipped1': {
img: '/fluent-emoji/1f587.png',
img: '/twemoji/1f587.svg',
bg: null,
frame: 'bronze',
},
'noteFavorited1': {
img: '/fluent-emoji/1f31f.png',
img: '/twemoji/1f31f.svg',
bg: null,
frame: 'bronze',
},
'profileFilled': {
img: '/fluent-emoji/1f44c.png',
img: '/twemoji/1f44c.svg',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'markedAsCat': {
img: '/fluent-emoji/1f408.png',
img: '/twemoji/1f408.svg',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'following1': {
img: '/fluent-emoji/2618.png',
img: '/twemoji/2618.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following10': {
img: '/fluent-emoji/1f6b8.png',
img: '/twemoji/1f6b8.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following50': {
img: '/fluent-emoji/1f91d.png',
img: '/twemoji/1f91d.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'following100': {
img: '/fluent-emoji/1f4af.png',
img: '/twemoji/1f4af.svg',
bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))',
frame: 'silver',
},
'following300': {
img: '/fluent-emoji/1f970.png',
img: '/twemoji/1f970.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers1': {
img: '/fluent-emoji/2618.png',
img: '/twemoji/2618.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'followers10': {
img: '/fluent-emoji/1f44b.png',
img: '/twemoji/1f44b.svg',
bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))',
frame: 'bronze',
},
'followers50': {
img: '/fluent-emoji/1f411.png',
img: '/twemoji/1f411.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'followers100': {
img: '/fluent-emoji/1f396.png',
img: '/twemoji/1f396.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers300': {
img: '/fluent-emoji/1f3c6.png',
img: '/twemoji/1f3c6.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'followers500': {
img: '/fluent-emoji/1f4e1.png',
img: '/twemoji/1f4e1.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'gold',
},
'followers1000': {
img: '/fluent-emoji/1f451.png',
img: '/twemoji/1f451.svg',
bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))',
frame: 'platinum',
},
'collectAchievements30': {
img: '/fluent-emoji/1f3c5.png',
img: '/twemoji/1f3c5.svg',
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
frame: 'silver',
},
'iLoveMisskey': {
img: '/fluent-emoji/2764.png',
img: '/twemoji/2764.svg',
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
frame: 'silver',
},
'client30min': {
img: '/fluent-emoji/1f552.png',
img: '/twemoji/1f552.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'noteDeletedWithin1min': {
img: '/fluent-emoji/1f5d1.png',
img: '/twemoji/1f5d1.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'postedAtLateNight': {
img: '/fluent-emoji/1f319.png',
img: '/twemoji/1f319.svg',
bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))',
frame: 'bronze',
},
'postedAt0min0sec': {
img: '/fluent-emoji/1f55b.png',
img: '/twemoji/1f55b.svg',
bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))',
frame: 'bronze',
},
'selfQuote': {
img: '/fluent-emoji/1f4dd.png',
img: '/twemoji/1f4dd.svg',
bg: null,
frame: 'bronze',
},
'htl20npm': {
img: '/fluent-emoji/1f30a.png',
img: '/twemoji/1f30a.svg',
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
'driveFolderCircularReference': {
img: '/fluent-emoji/1f4c2.png',
img: '/twemoji/1f4c2.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'reactWithoutRead': {
img: '/fluent-emoji/2753.png',
img: '/twemoji/2753.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'clickedClickHere': {
img: '/fluent-emoji/2757.png',
img: '/twemoji/2757.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'bronze',
},
'justPlainLucky': {
img: '/fluent-emoji/1f340.png',
img: '/twemoji/1f340.svg',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'silver',
},
'setNameToSyuilo': {
img: '/fluent-emoji/1f36e.png',
img: '/twemoji/1f36e.svg',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'passedSinceAccountCreated1': {
img: '/fluent-emoji/0031-20e3.png',
img: '/twemoji/0031-20e3.svg',
bg: null,
frame: 'bronze',
},
'passedSinceAccountCreated2': {
img: '/fluent-emoji/0032-20e3.png',
img: '/twemoji/0032-20e3.svg',
bg: null,
frame: 'silver',
},
'passedSinceAccountCreated3': {
img: '/fluent-emoji/0033-20e3.png',
img: '/twemoji/0033-20e3.svg',
bg: null,
frame: 'gold',
},
'loggedInOnBirthday': {
img: '/fluent-emoji/1f382.png',
img: '/twemoji/1f382.svg',
bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))',
frame: 'silver',
},
'cookieClicked': {
img: '/fluent-emoji/1f36a.png',
img: '/twemoji/1f36a.svg',
bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))',
frame: 'bronze',
},
'brainDiver': {
img: '/fluent-emoji/1f9e0.png',
img: '/twemoji/1f9e0.svg',
bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))',
frame: 'bronze',
},