Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nitrotanロールへの対応 #881

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export interface ConfigInterface {
meetingVote?: string
vcSpeechLog?: string
discussion?: string
other?: string
}
role?: {
admin?: string
verified?: string
nitrotan?: string
}
}
translateGasUrl?: string
Expand Down
27 changes: 24 additions & 3 deletions src/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ import { SearchImageCommand } from './commands/searchimg'
import { AkakeseCommand } from './commands/akakese'
import { ToenCommand } from './commands/toen'
import { UnmuteCommand } from './commands/unmute'
import { NitrotanReactionEvent } from './events/nitrotan-reaction'
import { NitrotanMessageEvent } from './events/nitrotan-message'
import { NitrotanOptimizeTask } from './tasks/nitrotan-optimize'
import { NitrotanProfileTask } from './tasks/nitrotan-profile'

export class Discord {
private config: Configuration
public readonly client: Client

private readonly tasks: BaseDiscordTask[] = []

public static readonly commands: BaseCommand[] = [
new AkakeseCommand(),
new AlphaCommand(),
Expand Down Expand Up @@ -126,6 +132,8 @@ export class Discord {
new MeetingNewVoteEvent(this),
new MeetingReactionVoteEvent(this),
new NewDiscussionMention(this),
new NitrotanMessageEvent(this),
new NitrotanReactionEvent(this),
new PinPrefixEvent(this),
new PinReactionEvent(this),
new VCSpeechLogMessageUrlEvent(this),
Expand All @@ -138,10 +146,17 @@ export class Discord {
Logger.configure('Discord.login').error('❌ login failed', error as Error)
})

const tasks: BaseDiscordTask[] = [new MeetingVoteTask(this)]
for (const task of tasks) {
this.tasks = [
new MeetingVoteTask(this),
new NitrotanOptimizeTask(this),
new NitrotanProfileTask(this),
]
for (const task of this.tasks) {
task.register().catch((error: unknown) => {
Logger.configure('Discord.task').error('❌ task failed', error as Error)
Logger.configure('Discord.task').error(
'❌ task register failed',
error as Error
)
})
}

Expand All @@ -168,6 +183,12 @@ export class Discord {
onReady() {
const logger = Logger.configure('Discord.onReady')
logger.info(`👌 ready: ${this.client.user?.tag}`)

for (const task of this.tasks) {
task.execute().catch((error: unknown) => {
logger.error('❌ task execute failed', error as Error)
})
}
}

async onMessageCreate(message: Message) {
Expand Down
140 changes: 140 additions & 0 deletions src/events/nitrotan-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { GuildPremiumTier, Message } from 'discord.js'
import { BaseDiscordEvent } from '.'
import {
Nitrotan,
NitrotanReason,
NitrotanReasonType,
} from '@/features/nitrotan'

/**
* Nitrotan判別で、メッセージ投稿に関するイベントのハンドラー
*/
export class NitrotanMessageEvent extends BaseDiscordEvent<'messageCreate'> {
readonly eventName = 'messageCreate'

private emojiRegex = /<(?<animated>a?):(?<name>\w+):(?<id>\d+)>/g

async execute(message: Message): Promise<void> {
if (!message.guild) return

const userId = message.author.id

const nitrotan = await Nitrotan.of(this.discord)
if (nitrotan.isNitrotan(userId)) return

const reason = this.getNitrotanReason(message)
if (!reason) return

await nitrotan.add(userId, reason)
}

/**
* Nitroユーザーが取れる行動かどうか。かつどんな操作であるか
*/
private getNitrotanReason(message: Message): NitrotanReasonType | null {
if (this.isAnimationEmojiMessage(message)) {
return NitrotanReason.USE_ANIMATION_EMOJI_MESSAGE
}
if (this.isOtherServerEmojiMessage(message)) {
return NitrotanReason.USE_OTHER_SERVER_EMOJI_MESSAGE
}
if (this.isCustomStickerMessage(message)) {
return NitrotanReason.USE_OTHER_SERVER_STICKER
}
if (this.isOverBoostFileMessage(message)) {
return NitrotanReason.SEND_LARGE_FILE
}
if (this.isOver2000CharactersMessage(message)) {
return NitrotanReason.SEND_LARGE_MESSAGE
}

return null
}

/**
* アニメーション絵文字を使用したメッセージを送信したか
*
* @param message メッセージ
* @returns アニメーション絵文字を使用したメッセージを送信したか
*/
private isAnimationEmojiMessage(message: Message): boolean {
const matches = message.content.matchAll(this.emojiRegex)

return [...matches].some((match) => match.groups?.animated === 'a')
}

/**
* 他サーバーの絵文字を使用したメッセージを送信したか
*
* @param message メッセージ
* @returns 他サーバーの絵文字を使用したメッセージを送信したか
*/
private isOtherServerEmojiMessage(message: Message): boolean {
const matches = message.content.matchAll(this.emojiRegex)

const guildEmojis = message.guild?.emojis.cache ?? new Map<string, string>()

return [...matches].some((match) => {
const emojiId = match.groups?.id
if (!emojiId) return false
return !guildEmojis.has(emojiId)
})
}

/**
* サーバにないカスタムスタンプを使用したか
*
* @param message メッセージ
* @returns サーバにないカスタムスタンプを使用したか
*/
private isCustomStickerMessage(message: Message): boolean {
const stickers = message.stickers

const guildStickers =
message.guild?.stickers.cache ?? new Map<string, string>()

return stickers.some((sticker) => {
const stickerId = sticker.id
if (!stickerId) return false
return !guildStickers.has(stickerId)
})
}

/**
* ブーストによる上限値より大きなファイルを送信したか
*
* @param message メッセージ
* @returns ブーストによる上限値より大きなファイルを送信したか
*/
private isOverBoostFileMessage(message: Message): boolean {
const premiumTier = message.guild?.premiumTier ?? 0

const maxFileSize = (() => {
switch (premiumTier) {
case GuildPremiumTier.Tier2: {
return 50
} // ブーストLv2
case GuildPremiumTier.Tier3: {
return 100
} // ブーストLv3
default: {
return 25
} // 通常、またはブーストLv1
}
})()

return message.attachments.some(
(attachment) => attachment.size > maxFileSize * 1024 * 1024
)
}

/**
* 2000文字より多い文章を投稿したか
*
* @param message メッセージ
* @returns 2000文字より多い文章を投稿したか
*/
private isOver2000CharactersMessage(message: Message): boolean {
return message.content.length > 2000
}
}
88 changes: 88 additions & 0 deletions src/events/nitrotan-reaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
MessageReaction,
PartialMessageReaction,
PartialUser,
User,
} from 'discord.js'
import { BaseDiscordEvent } from '.'
import {
Nitrotan,
NitrotanReason,
NitrotanReasonType,
} from '@/features/nitrotan'

/**
* Nitrotan判別で、メッセージ投稿に関するイベントのハンドラー
*/
export class NitrotanReactionEvent extends BaseDiscordEvent<'messageReactionAdd'> {
readonly eventName = 'messageReactionAdd'

// アニメーション絵文字をリアクションした
// 他サーバーの絵文字をリアクションした
// スーパーリアクションを使用した

async execute(
reaction: MessageReaction | PartialMessageReaction,
user: User | PartialUser
): Promise<void> {
// 部分的なオブジェクトをフェッチ
reaction = reaction.partial ? await reaction.fetch() : reaction
user = user.partial ? await user.fetch() : user

const message = reaction.message.partial
? await reaction.message.fetch()
: reaction.message
if (!message.guild) return

const nitrotan = await Nitrotan.of(this.discord)
if (nitrotan.isNitrotan(user.id)) return

const reason = this.getNitrotanReason(reaction)
if (!reason) return

await nitrotan.add(user.id, reason)
}

/**
* Nitroユーザーが取れる行動かどうか。かつどんな操作であるか
*/
private getNitrotanReason(
reaction: MessageReaction
): NitrotanReasonType | null {
if (this.isAnimationEmojiReaction(reaction)) {
return NitrotanReason.USE_ANIMATION_EMOJI_REACTION
}
if (this.isOtherServerEmojiReaction(reaction)) {
return NitrotanReason.USE_OTHER_SERVER_EMOJI_REACTION
}

return null
}

/**
* アニメーション絵文字を最初にリアクションしたか
*
* @param reaction リアクション
* @returns アニメーション絵文字を最初にリアクションしたか
*/
private isAnimationEmojiReaction(reaction: MessageReaction): boolean {
if (reaction.count !== 1) return false

return reaction.emoji.animated ?? false
}

/**
* 他サーバーの絵文字を最初にリアクションしたか
*
* @param reaction リアクション
* @returns 他サーバーの絵文字を最初にリアクションしたか
*/
private isOtherServerEmojiReaction(reaction: MessageReaction): boolean {
if (reaction.count !== 1) return false

return (
'guild' in reaction.emoji &&
reaction.emoji.guild.id === reaction.message.guild?.id
)
}
}
Loading