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

View File

@ -239,6 +239,19 @@ export class UserProfile {
}) })
public mutingNotificationTypes: typeof notificationTypes[number][]; 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 //#region Denormalized fields
@Index() @Index()
@Column("varchar", { @Column("varchar", {

View File

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

View File

@ -81,5 +81,10 @@ export const packedNotificationSchema = {
optional: true, optional: true,
nullable: true, nullable: true,
}, },
achievement: {
type: "object",
optional: true,
nullable: true,
},
}, },
} as const; } 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_2fa_unregister from "./endpoints/i/2fa/unregister.js";
import * as ep___i_apps from "./endpoints/i/apps.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_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_changePassword from "./endpoints/i/change-password.js";
import * as ep___i_deleteAccount from "./endpoints/i/delete-account.js"; import * as ep___i_deleteAccount from "./endpoints/i/delete-account.js";
import * as ep___i_exportBlocking from "./endpoints/i/export-blocking.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_search from "./endpoints/users/search.js";
import * as ep___users_show from "./endpoints/users/show.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_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___fetchRss from "./endpoints/fetch-rss.js";
import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js"; import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js";
import * as ep___bites_create from "./endpoints/bites/create.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/2fa/unregister", ep___i_2fa_unregister],
["i/apps", ep___i_apps], ["i/apps", ep___i_apps],
["i/authorized-apps", ep___i_authorizedApps], ["i/authorized-apps", ep___i_authorizedApps],
["i/claim-achievement", ep___i_claimAchievement],
["i/change-password", ep___i_changePassword], ["i/change-password", ep___i_changePassword],
["i/delete-account", ep___i_deleteAccount], ["i/delete-account", ep___i_deleteAccount],
["i/export-blocking", ep___i_exportBlocking], ["i/export-blocking", ep___i_exportBlocking],
@ -681,6 +684,7 @@ const eps = [
["users/search", ep___users_search], ["users/search", ep___users_search],
["users/show", ep___users_show], ["users/show", ep___users_show],
["users/stats", ep___users_stats], ["users/stats", ep___users_stats],
["users/achievements", ep___users_achievements],
["admin/drive-capacity-override", ep___admin_driveCapOverride], ["admin/drive-capacity-override", ep___admin_driveCapOverride],
["fetch-rss", ep___fetchRss], ["fetch-rss", ep___fetchRss],
["get-sounds", ep___sounds], ["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"; import define from "../define.js";
export const meta = { export const meta = {
@ -23,9 +23,27 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user, token) => { export default define(meta, paramDef, async (ps, user, token) => {
const isSecure = token == null; const isSecure = token == null;
// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す const now = new Date();
return await Users.pack<true, true>(user.id, user, { 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, detail: true,
includeSecrets: isSecure, 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> <template>
<div> <div>
<div v-if="achievements" :class="'.root'"> <div v-if="achievements" class="root">
<div v-for="achievement in achievements" :key="achievement" :class="'.achievement'" class="_panel"> <div v-for="achievement in achievements" :key="achievement" class="_panel achievement">
<div :class="'.icon'"> <div class="icon">
<div :class="['.iconFrame', ['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]"> <div :class="[['iconFrame'], ['iconFrame_' + ACHIEVEMENT_BADGES[achievement.name].frame]]">
<div :class="['.iconInner']" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }"> <div class="iconInner" :style="{ background: ACHIEVEMENT_BADGES[achievement.name].bg }">
<img :class="'.iconImg'" :src="ACHIEVEMENT_BADGES[achievement.name].img"> <img class="iconImg" :src="ACHIEVEMENT_BADGES[achievement.name].img">
</div> </div>
</div> </div>
</div> </div>
<div :class="'.body'"> <div class="body">
<div :class="'.header'"> <div class="header">
<span :class="'.title'">{{ i18n.ts._achievements._types['_' + achievement.name].title }}</span> <span class="title">{{ i18n.ts._achievements._types['_' + achievement.name].title }}</span>
<span :class="'.time'"> <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> <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> </span>
</div> </div>
<div :class="'.description'">{{ i18n.ts._achievements._types['_' + achievement.name].description }}</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 v-if="i18n.ts._achievements._types['_' + achievement.name].flavor" class="flavor">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
</div> </div>
</div> </div>
<template v-if="withLocked"> <template v-if="withLocked">
<div v-for="achievement in lockedAchievements" :key="achievement" :class="['.achievement', '.locked']" class="_panel" @click="achievement === 'clickedClickHere' ? clickHere() : () => {}"> <div v-for="achievement in lockedAchievements" :key="achievement" class="_panel achievement locked" @click="achievement === 'clickedClickHere' ? clickHere() : () => {}">
<div :class="'.icon'"> <div class="icon">
</div> </div>
<div :class="'.body'"> <div class="body">
<div :class="'.header'"> <div class="header">
<span :class="'.title'">???</span> <span class="title">???</span>
</div> </div>
<div :class="'.description'">???</div> <div class="description">???</div>
</div> </div>
</div> </div>
</template> </template>
@ -77,7 +77,7 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss" module> <style lang="scss" scoped>
.root { .root {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, min(380px, 100%)); grid-template-columns: repeat(auto-fill, min(380px, 100%));

View File

@ -997,7 +997,7 @@ async function post() {
} }
const text = postData.text?.toLowerCase() ?? ''; 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'); claimAchievement('iLoveMisskey');
} }
if (text.includes('Efrlqw8ytg4'.toLowerCase()) || text.includes('XVCwzwxdHuA'.toLowerCase())) { if (text.includes('Efrlqw8ytg4'.toLowerCase()) || text.includes('XVCwzwxdHuA'.toLowerCase())) {

View File

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

View File

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