Skip to content

Commit

Permalink
feat: 毎日0時に#generalへメッセージを送信する機能の追加 (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
book000 authored Dec 16, 2023
1 parent 0a12786 commit 7ae5e89
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": false
"source.organizeImports": "never"
}
},
"git.branchProtection": ["main", "master"],
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ RUN pnpm install --frozen-lockfile --offline

ENV NODE_ENV production
ENV CONFIG_PATH /data/config.json
ENV DATA_DIR /data
ENV LOG_DIR /data/logs

VOLUME [ "/data" ]
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"devDependencies": {
"@book000/node-utils": "1.12.2",
"@types/node": "20.8.7",
"@types/node-cron": "3.0.11",
"@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.14.0",
"axios": "1.6.2",
Expand All @@ -36,6 +37,7 @@
"eslint-plugin-n": "16.4.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "49.0.0",
"node-cron": "3.0.3",
"prettier": "3.1.1",
"tsx": "4.6.2",
"typescript": "5.3.3",
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import { ToswjaCommand } from './commands/toswja'
import { TozhCommand } from './commands/tozh'
import { TozhjaCommand } from './commands/tozhja'
import { NewDiscussionMention } from './events/new-discussion-mention'
import { BaseDiscordJob } from './jobs'
import nodeCron from 'node-cron'
import { EveryDayJob } from './jobs/everyday'

export class Discord {
private config: Configuration
Expand Down Expand Up @@ -119,6 +122,11 @@ export class Discord {
task.register()
}

const crons: BaseDiscordJob[] = [new EveryDayJob(this)]
for (const job of crons) {
job.register(nodeCron)
}

this.config = config
}

Expand Down
63 changes: 63 additions & 0 deletions src/features/birthday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import fs from 'node:fs'

export interface IBirthday {
/** DiscordのユーザーID */
discordId: string
/** 誕生日 */
birthday: Date
/** 強制する年齢 */
age?: number
}

export class Birthday {
private birthdays: IBirthday[] = []

constructor() {
this.load()
}

public load() {
const path = this.getPath()

if (!fs.existsSync(path)) {
return
}

const data = fs.readFileSync(path, 'utf8')
const birthdays = JSON.parse(data) as IBirthday[]
this.birthdays = birthdays.map((birthday) => ({
...birthday,
birthday: new Date(birthday.birthday),
}))
}

public get(date: Date): IBirthday[] {
return this.birthdays.filter(
(birthday) =>
birthday.birthday.getMonth() === date.getMonth() &&
birthday.birthday.getDate() === date.getDate()
)
}

public static getAge(birthday: IBirthday): number {
if (birthday.age) {
return birthday.age
}
const today = new Date()
const age = today.getFullYear() - birthday.birthday.getFullYear()
if (
birthday.birthday.getMonth() > today.getMonth() ||
(birthday.birthday.getMonth() === today.getMonth() &&
birthday.birthday.getDate() > today.getDate())
) {
return age - 1
}
return age
}

private getPath() {
const dataDir = process.env.DATA_DIR ?? 'data/'

return `${dataDir}/birthday.json`
}
}
123 changes: 123 additions & 0 deletions src/jobs/everyday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Birthday } from '@/features/birthday'
import { BaseDiscordJob } from '.'
import { Configuration } from '@/config'
import { ChannelType } from 'discord.js'
import { Kinenbi } from '@/features/kinenbi'

/**
* 毎日0時に#generalへメッセージを送信する
*/
export class EveryDayJob extends BaseDiscordJob {
get schedule(): string {
// 毎日0時
return '0 0 * * *'
}

async execute(): Promise<void> {
const config: Configuration = this.discord.getConfig()
const meetingVoteChannelId =
config.get('discord').channel?.general || '1138605147287728150'

const channel =
await this.discord.client.channels.fetch(meetingVoteChannelId)
if (!channel || channel.type !== ChannelType.GuildText) return

// 誕生日を取得
const birthday = new Birthday()

const today = new Date()
const todayBirthdays = birthday.get(today)

// 記念日を取得
const kinenbiContents = await this.getKinenbiContents(today)

// メッセージに必要な情報を取得
// 今日の日付 (yyyy年mm月dd日)と曜日
const todayString = `${today.getFullYear()}${
today.getMonth() + 1
}${today.getDate()}日`
const todayWeek = ['日', '月', '火', '水', '木', '金', '土'][today.getDay()]

// 年間通算日数
const yearStart = new Date(today.getFullYear(), 0, 1)
const yearEnd = new Date(today.getFullYear(), 11, 31)
// 今年の経過日数
const yearPassed = Math.floor(
(today.getTime() - yearStart.getTime()) / (1000 * 60 * 60 * 24)
)
// 年日数
const yearTotal = Math.floor(
(yearEnd.getTime() - yearStart.getTime()) / (1000 * 60 * 60 * 24)
)
// 残り日数
const yearLeft = yearTotal - yearPassed
// 残りパーセント (小数点以下2桁切り捨て)
const yearLeftPercent = Math.floor((yearLeft / yearTotal) * 10_000) / 100

// ------------------------------

const otherCongratulation = [
':congratulations: __**その他の今日の記念日**__ :congratulations:',
'',
...todayBirthdays.map((birthday) => {
const age = Birthday.getAge(birthday)
const ageText = age < 23 ? `(${age}歳)` : ''
return `- <@${birthday.discordId}>の誕生日 ${ageText}`
}),
]

// メッセージを作成
const content = [
`:loudspeaker:__**${todayString} (${todayWeek}曜日)**__:loudspeaker:`,
`**年間通算 ${yearPassed} 日目**`,
`**年間残り ${yearLeft} 日 (${yearLeftPercent}%)**`,
'',
':birthday: __**今日の誕生日 (一般社団法人 日本記念日協会)**__ :birthday:',
'記念日名の前にある番号(記念日ナンバー)を使って、記念日を詳しく調べられます。`/origin <記念日ナンバー>`を実行して下さい。(当日のみ)',
'',
...kinenbiContents,
'',
todayBirthdays.length > 0 ? otherCongratulation.join('\n') : '',
]

// メッセージを送信
await channel.send(content.join('\n').trim())
}

private async getKinenbiContents(date: Date): Promise<string[]> {
const kinenbi = new Kinenbi()
const todayKinenbi = await kinenbi.get(date)
const contents = []

const isAlreadyAddedDetail = false

if (todayKinenbi.length === 0) {
contents.push('本日の記念日が存在しないか、取得できませんでした。')
return contents
}

let num = 0
for (const result of todayKinenbi) {
num++
if (result.title.includes('えのすいクラゲ')) {
contents.push(`${num}. ${result.title} <@372701608053833730>`)
continue
}

if (isAlreadyAddedDetail) {
contents.push(`${num}. ${result.title}`)
continue
}

const detail = await kinenbi.getDetail(result.detail)
if (detail.description.includes('毎月')) {
contents.push(`${num}. ${result.title}`)
continue
}

contents.push(`${num}. ${result.title} \`\`\`${detail.description}\`\`\``)
}

return contents
}
}
27 changes: 27 additions & 0 deletions src/jobs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Discord } from '../discord'
import nodeCron from 'node-cron'

export abstract class BaseDiscordJob {
protected readonly discord: Discord

constructor(discord: Discord) {
this.discord = discord
}

/** スケジュール */
abstract get schedule(): string

register(cron: typeof nodeCron) {
cron.schedule(
this.schedule,
async () => {
await this.execute()
},
{
timezone: 'Asia/Tokyo',
}
)
}

abstract execute(): Promise<void>
}

0 comments on commit 7ae5e89

Please sign in to comment.