Skip to content

Commit

Permalink
refector: 整理代码结构
Browse files Browse the repository at this point in the history
  • Loading branch information
LemonNekoGH committed Jun 2, 2022
1 parent 8dbb244 commit 27cf462
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 106 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ module.exports = {
],
parserOptions: {
project: './tsconfig.json'
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off'
}
}
22 changes: 22 additions & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { readFile } from 'fs/promises'
import { getLogger } from 'log4js'
import { registerFont } from 'ultimate-text-to-image'

export let defaultAvatar: Buffer
export let mask: Buffer

export const loadAssets = async (): Promise<void> => {
// 获取 logger
const logger = getLogger()
// 读取默认的头像文件
defaultAvatar = await readFile('./src/assets/default_profile.png')
logger.debug('读取到了默认的头像文件')
// 读取遮罩文件
mask = await readFile('./src/assets/gradient-mask.png')
logger.debug('读取到了遮罩文件')
// 读取字体文件
registerFont('./src/assets/Alibaba-PuHuiTi-Regular.ttf', {
family: 'AliBabaPuHui'
})
logger.debug('已注册字体文件')
}
80 changes: 80 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { InputFile } from 'grammy'
import { getLogger } from 'log4js'
import { defaultAvatar, mask } from './assets'
import { token } from './config'
import { Commnad, MyHandler } from './types'
import { getArgsFromMessageText, jimpToInputFile, makeItAQuote } from './utils'

const logger = getLogger()

// 处理 quote 命令
const handleQuoteCommand: MyHandler = async (ctx) => {
const msg = ctx.message!
const { message_id: messageId } = msg
const { id: chatId } = ctx.chat
// 进行一些错误的判断
const replyMsg = ctx.message?.reply_to_message
if (typeof replyMsg === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复的消息`)
await ctx.reply('你并没有回复任何人哦', {
reply_to_message_id: ctx.message?.message_id
})
return
}
const sender = replyMsg.from
if (typeof sender === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复者`)
await ctx.reply('你回复的这条消息可能来自一个频道,获取不到作者呢', {
reply_to_message_id: ctx.message?.message_id
})
return
}
if (typeof replyMsg.text === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复消息的内容`)
await ctx.reply('你回复的这条消息没有内容呢', {
reply_to_message_id: ctx.message?.message_id
})
return
}
const { message_id: replyId } = await ctx.reply('正在进行处理,请稍等...')
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 已成功发送“处理中”提示信息`)
// 进行参数处理
const args = getArgsFromMessageText(msg.text)
// 被回复者 id
const username = sender.username ?? 'no_name'
// 被回复的消息内容
const text = replyMsg.text
// 被回复者的头像,这里取第一个
const avatar = (await ctx.api.getUserProfilePhotos(sender.id)).photos
let quoted: InputFile | undefined
if (avatar.length === 0) {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者是没有头像的`)
// 如果没有头像,使用默认头像进行图片的合成
const res = await makeItAQuote(defaultAvatar, mask, username, text, args)
quoted = await jimpToInputFile(res)
} else {
// 有头像,使用头像组中的第一个进行图片的合成
const photo = avatar[0][0]
// 尝试获取此文件
const file = await ctx.api.getFile(photo.file_id)
if (typeof file.file_path === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者头像文件获取到了,但是没有路径`)
return
}
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者头像在 https://api.telegram.org/file/bot${token}/${file.file_path}`)
const res = await makeItAQuote(`https://api.telegram.org/file/bot${token}/${file.file_path}`, mask, username, text, args)
quoted = await jimpToInputFile(res)
}
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 图片处理完成`)
await ctx.replyWithPhoto(quoted, {
reply_to_message_id: messageId
})
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 图片发送完成`)
await ctx.api.deleteMessage(chatId, replyId)
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 提示信息删除完成`)
}

// 导出命令集
export const commands: Commnad[] = [
new Commnad('quote', handleQuoteCommand)
]
9 changes: 9 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import axios from 'axios'
import { getLogger } from 'log4js'

export let token: string
export let notifyChatId: string

/**
* 从环境变量或参数中获取参数
* @param envVarName 环境变量名
Expand Down Expand Up @@ -55,3 +58,9 @@ export const getBotToken = async (): Promise<string> => {
logger.debug('token 是有效的')
return token
}

// 初始化配置
export const initConfig = async (): Promise<void> => {
token = await getBotToken()
notifyChatId = await getEnvVarOrArg('NOTIFY_CHAT_ID', '--notify-chat-id=', '通知聊天 ID')
}
101 changes: 11 additions & 90 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,26 @@
import { getBotToken, getEnvVarOrArg } from './config'
import { initConfig, notifyChatId, token } from './config'
import log4js from 'log4js'
import { Bot, InputFile } from 'grammy'
import { readFile } from 'fs/promises'
import { getArgsFromMessageText, jimpToInputFile, makeItAQuote } from './utils'
import { registerFont } from 'ultimate-text-to-image'
import { Bot } from 'grammy'
import { loadAssets } from './assets'
import { useCommands } from './utils'
import { commands } from './commands'

const main = async (): Promise<void> => {
// 配置 logger
log4js.configure({
appenders: { default: { type: 'console' } },
categories: { default: { appenders: ['default'], level: 'debug' } }
})
// 获取 token
const token = await getBotToken()
const notifyChatId = await getEnvVarOrArg('NOTIFY_CHAT_ID', '--notify=', '启动时通知到的对话 id')
// 初始化配置
await initConfig()
// 获取 logger
const logger = log4js.getLogger()
// 读取默认的头像文件
const defaultAvatar = await readFile('./src/assets/default_profile.png')
logger.debug('读取到了默认的头像文件')
// 读取遮罩文件
const mask = await readFile('./src/assets/gradient-mask.png')
logger.debug('读取到了遮罩文件')
// 读取字体文件
registerFont('./src/assets/Alibaba-PuHuiTi-Regular.ttf', {
family: 'AliBabaPuHui'
})
logger.debug('已注册字体')
// 加载资源
await loadAssets()
// 构建一个 bot
const bot = new Bot(token)
// 处理 quote 命令
bot.command('quote', async (ctx) => {
const msg = ctx.message
if (typeof msg === 'undefined') {
logger.error('收到了指令,但是获取不到对话 id')
return
}
const { message_id: messageId } = msg
const { id: chatId } = ctx.chat
logger.debug(`收到了来自 ${chatId} 的 quote 指令,消息 id: ${messageId}`)
// 进行一些错误的判断
const replyMsg = ctx.message?.reply_to_message
if (typeof replyMsg === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复的消息`)
await ctx.reply('你并没有回复任何人哦', {
reply_to_message_id: ctx.message?.message_id
})
return
}
const sender = replyMsg.from
if (typeof sender === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复者`)
await ctx.reply('你回复的这条消息可能来自一个频道,获取不到作者呢', {
reply_to_message_id: ctx.message?.message_id
})
return
}
if (typeof replyMsg.text === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 没有获取到被回复消息的内容`)
await ctx.reply('你回复的这条消息没有内容呢', {
reply_to_message_id: ctx.message?.message_id
})
return
}
const { message_id: replyId } = await ctx.reply('正在进行处理,请稍等...')
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 已成功发送“处理中”提示信息`)
// 进行参数处理
const args = getArgsFromMessageText(msg.text)
// 被回复者 id
const username = sender.username ?? 'no_name'
// 被回复的消息内容
const text = replyMsg.text
// 被回复者的头像,这里取第一个
const avatar = (await ctx.api.getUserProfilePhotos(sender.id)).photos
let quoted: InputFile | undefined
if (avatar.length === 0) {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者是没有头像的`)
// 如果没有头像,使用默认头像进行图片的合成
const res = await makeItAQuote(defaultAvatar, mask, username, text, args)
quoted = await jimpToInputFile(res)
} else {
// 有头像,使用头像组中的第一个进行图片的合成
const photo = avatar[0][0]
// 尝试获取此文件
const file = await ctx.api.getFile(photo.file_id)
if (typeof file.file_path === 'undefined') {
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者头像文件获取到了,但是没有路径`)
return
}
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 被回复的消息作者头像在 https://api.telegram.org/file/bot${token}/${file.file_path}`)
const res = await makeItAQuote(`https://api.telegram.org/file/bot${token}/${file.file_path}`, mask, username, text, args)
quoted = await jimpToInputFile(res)
}
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 图片处理完成`)
await ctx.replyWithPhoto(quoted, {
reply_to_message_id: messageId
})
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 图片发送完成`)
await ctx.api.deleteMessage(chatId, replyId)
logger.debug(`[chat: ${chatId}, command: quote, msg: ${messageId}] 提示信息删除完成`)
})
// 使用命令处理器集
useCommands(bot, commands)
// 错误处理
bot.catch(e => {
const err = e as Error
Expand Down
50 changes: 50 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Bot, Middleware } from 'grammy'
import { getLogger } from 'log4js'

/**
* 命令参数
* 从消息中获取
*/
export interface ArgsFromMsg {
quoteMarkLeft: string // 自定义引号字符,左侧
quoteMarkRight: string // 自定义引号字符,右侧
gray: boolean // 是否把头像处理成灰色
}

// 获取 Bot 的 command 方法类型
type GetCommandFn = Bot['command']
export type BotCommandHandler = GetCommandFn extends (f: any, r: infer Rest) => any ? Rest : never
export type CommandCtx = BotCommandHandler extends Middleware<infer Ctx> ? Ctx : never
export type MyHandler = (ctx: CommandCtx) => Promise<void>

/**
* 命令
*/
export class Commnad {
name: string // 命令名称
fn: MyHandler // 命令处理函数

constructor (name: string, fn: MyHandler) {
this.name = name
this.fn = fn
}

// 让 Bot 使用此命令
use = (bot: Bot): void => {
bot.command(this.name, async (ctx) => {
const logger = getLogger()
try {
const msg = ctx.message
if (typeof msg === 'undefined') {
logger.info(`收到 ${this.name} 命令,但获取不到消息内容,对话 ID: ${ctx.chat.id}`)
return
}
logger.info(`收到 ${this.name} 命令,对话 ID: ${ctx.chat.id},消息 ID: ${msg.message_id}`)
// 尝试执行命令处理器
await this.fn(ctx)
} catch (e) {
logger.error(`[${this.name} 命令,对话 ID: ${ctx.chat.id}] 处理出错:`, e)
}
})
}
}
34 changes: 18 additions & 16 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import { InputFile } from 'grammy'
import { Bot, InputFile } from 'grammy'
import Jimp from 'jimp'
import { getLogger } from 'log4js'
// import path from 'path'
import { UltimateTextToImage, VerticalImage } from 'ultimate-text-to-image'

/**
* 命令参数
*/
export interface Args {
quoteMarkLeft: string // 自定义引号字符,左侧
quoteMarkRight: string // 自定义引号字符,右侧
gray: boolean // 是否把头像处理成灰色
}

import { ArgsFromMsg, Commnad } from './types'
/**
* 把任意头像、id、文字转成一张图片
* @param avatar 头像,buffer 类型
* @param id 会在前面加上一个 @
* @param text 图片正文
*/
export const makeItAQuote = async (avatarIn: Buffer | string, maskIn: Buffer, idIn: string, textIn: string, commandArgs: Args): Promise<Jimp> => {
export const makeItAQuote = async (avatarIn: Buffer | string, maskIn: Buffer, idIn: string, textIn: string, commandArgs: ArgsFromMsg): Promise<Jimp> => {
const logger = getLogger()
let avatar: Jimp
// 这个类型判断是为了通过类型检查
Expand Down Expand Up @@ -50,7 +41,7 @@ export const makeItAQuote = async (avatarIn: Buffer | string, maskIn: Buffer, id
* @param id 会在前面加上一个 @
* @param text 图片正文
*/
export const genTextWithIdPic = async (id: string, text: string, commandArgs: Args): Promise<Buffer> => {
export const genTextWithIdPic = async (id: string, text: string, commandArgs: ArgsFromMsg): Promise<Buffer> => {
const logger = getLogger()
logger.debug(`使用了引号 ${commandArgs.quoteMarkLeft}${commandArgs.quoteMarkRight}`)
const image = new VerticalImage([
Expand Down Expand Up @@ -85,7 +76,7 @@ export const jimpToInputFile = async (src: Jimp): Promise<InputFile> => {
}

// 从消息文本中获取参数
export const getArgsFromMessageText = ((): ((text: string) => Args) => {
export const getArgsFromMessageText = ((): ((text: string) => ArgsFromMsg) => {
// 获取参数的方法,不需要外部可以访问
const getArgValue = (rawArgs: string[], prefix: string): string => {
for (const arg0 of rawArgs) {
Expand All @@ -104,9 +95,9 @@ export const getArgsFromMessageText = ((): ((text: string) => Args) => {
}
return false
}
return (text: string): Args => {
return (text: string): ArgsFromMsg => {
// 默认参数
const defaultArgs: Args = {
const defaultArgs: ArgsFromMsg = {
quoteMarkLeft: '"',
quoteMarkRight: '"',
gray: false
Expand All @@ -122,3 +113,14 @@ export const getArgsFromMessageText = ((): ((text: string) => Args) => {
return defaultArgs
}
})()

/**
* 使用命令集
* @param bot 机器人实例
* @param commands
*/
export const useCommands = <B extends Bot>(bot: B, commands: Commnad[]): void => {
for (const command of commands) {
command.use(bot)
}
}

0 comments on commit 27cf462

Please sign in to comment.