Skip to content

Commit

Permalink
feat(line): add adapter-line
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Jul 12, 2023
1 parent 3354eea commit 08bb513
Show file tree
Hide file tree
Showing 10 changed files with 1,845 additions and 0 deletions.
33 changes: 33 additions & 0 deletions adapters/line/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@satorijs/adapter-line",
"description": "Line Adapter for Satorijs",
"version": "1.0.0",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib"
],
"author": "LittleC <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/satorijs/satori.git",
"directory": "adapters/line"
},
"bugs": {
"url": "https://github.com/satorijs/satori/issues"
},
"homepage": "https://koishi.chat/plugins/adapter/line.html",
"keywords": [
"bot",
"line",
"adapter",
"chatbot",
"satori"
],
"peerDependencies": {
"@satorijs/satori": "^2.4.0"
},
"dependencies": {},
"devDependencies": {}
}
108 changes: 108 additions & 0 deletions adapters/line/src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Bot, Context, Quester, Schema, Universal } from '@satorijs/satori'
import { HttpServer } from './http'
import { Internal } from './types'
import { LineMessageEncoder } from './message'

export class LineBot extends Bot<LineBot.Config> {
static MessageEncoder = LineMessageEncoder
public http: Quester
public contentHttp: Quester
public internal: Internal
constructor(ctx: Context, config: LineBot.Config) {
super(ctx, config)
ctx.plugin(HttpServer, this)
this.http = ctx.http.extend({
...config.api,
headers: {
Authorization: `Bearer ${config.token}`,
},
})
this.contentHttp = ctx.http.extend({
...config.content,
headers: {
Authorization: `Bearer ${config.token}`,
},
})
this.internal = new Internal(this.http)
}

// https://developers.line.biz/en/reference/messaging-api/#get-profile
async getSelf(): Promise<Universal.User> {
const { userId, displayName, pictureUrl } = await this.internal.getBotInfo()
return {
userId,
nickname: displayName,
avatar: pictureUrl,
}
}

async getFriendList(): Promise<Universal.User[]> {
let userIds: string[] = []
let start: string
do {
const res = await this.internal.getFollowers(start, 1000)
userIds = userIds.concat(res.userIds)
start = res.next
} while (start)

return userIds.map(v => ({ userId: v }))
}

async getGuild(guildId: string): Promise<Universal.Guild> {
const res = await this.internal.getGroupSummary(guildId)
return {
guildId: res.groupId,
guildName: res.groupName,
}
}

async getGuildMemberList(guildId: string): Promise<Universal.GuildMember[]> {
let userIds: string[] = []
let start: string
do {
const res = await this.internal.getGroupMembersIds(guildId, start)
userIds = userIds.concat(res.memberIds)
start = res.next
} while (start)

return userIds.map(v => ({ userId: v }))
}

async getGuildMember(guildId: string, userId: string): Promise<Universal.GuildMember> {
const res = await this.internal.getGroupMemberProfile(guildId, userId)
return ({
userId: res.userId,
nickname: res.displayName,
avatar: res.pictureUrl,
})
}
}

export namespace LineBot {
export interface Config extends Bot.Config {
privateKey: string
secret: string
kid?: string
channelId: string
token: string
api: Quester.Config
content: Quester.Config
}
export const Config: Schema<Config> = Schema.intersect([
Schema.object({
api: Quester.createConfig('https://api.line.me/'),
}),
Schema.object({
content: Quester.createConfig('https://api-data.line.me/'),
}),
Schema.object({
privateKey: Schema.string().role('secret'),
secret: Schema.string().role('secret'),
kid: Schema.string(),
channelId: Schema.string(),
token: Schema.string().required(),
}),
] as const)
}

LineBot.prototype.platform = 'line'
34 changes: 34 additions & 0 deletions adapters/line/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Adapter, Context, Logger } from '@satorijs/satori'
import { LineBot } from './bot'
import crypto from 'node:crypto'
import { WebhookRequestBody } from './types'
import { adaptSessions } from './utils'

export class HttpServer extends Adapter.Server<LineBot> {
logger = new Logger('line')
constructor(ctx: Context, bot: LineBot) {
super()
}

async start(bot: LineBot) {
const { userId } = await bot.internal.getBotInfo()
bot.selfId = userId
bot.online()
bot.ctx.router.post('/line', async (ctx) => {
const sign = ctx.headers['x-line-signature']?.toString()
const hash = crypto.createHmac('SHA256', bot.config.secret).update(ctx.request.rawBody || '').digest('base64')
if (hash !== sign) {
return ctx.status = 403
}
const parsed = ctx.request.body as WebhookRequestBody
this.logger.debug(require('util').inspect(parsed, false, null, true))
for (const event of parsed.events) {
const sessions = await adaptSessions(bot, event)
if (sessions.length) sessions.forEach(bot.dispatch.bind(bot))
this.logger.debug(require('util').inspect(sessions, false, null, true))
}
ctx.status = 200
ctx.body = 'ok'
})
}
}
9 changes: 9 additions & 0 deletions adapters/line/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LineBot } from './bot'

export * from './bot'
export * from './utils'
export * from './types'
export * from './http'
export * from './message'

export default LineBot
107 changes: 107 additions & 0 deletions adapters/line/src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { h, MessageEncoder } from '@satorijs/satori'
import { LineBot } from './bot'
import { Message, Sender, TextMessage } from './types'

export const escape = (val: string) =>
val
.replace(/(?<!\u200b)[\*_~`]/g, '\u200B$&')

export const unescape = (val: string) =>
val
.replace(/\u200b([\*_~`])/g, '$1')

export class LineMessageEncoder extends MessageEncoder<LineBot> {
buffer = ''
blocks: Message[] = []
block: Message = null
sender: Sender = {}
emojis: TextMessage['emojis'] = []
async flush(): Promise<void> {
await this.insertBlock()
// https://developers.line.biz/en/reference/messaging-api/#send-push-message
for (let i = 0; i < this.blocks.length; i += 5) {
await this.bot.http.post('/v2/bot/message/push', {
to: this.channelId,
messages: this.blocks.slice(i, i + 5),
}, {
headers: {
Authorization: `Bearer ${this.bot.config.token}`,
},
})
}
}

async insertBlock() {
if (this.buffer.length) {
this.blocks.push({
...{
type: 'text',
text: escape(this.buffer),
sender: { ...this.sender },
},
...this.emojis.length ? { emojis: this.emojis } : {},
})
this.buffer = ''
this.emojis = []
}
}

async visit(element: h) {
const { type, attrs, children } = element

if (type === 'text') {
this.buffer += attrs.content
} else if (type === 'image' && attrs.url) {
await this.insertBlock()
this.blocks.push({
type: 'image',
originalContentUrl: attrs.url,
previewImageUrl: attrs.url,
})
} else if (type === 'video' && attrs.url) {
await this.insertBlock()
this.blocks.push({
type: 'video',
originalContentUrl: attrs.url,
previewImageUrl: attrs.url,
})
} else if (type === 'audio' && attrs.url) {
await this.insertBlock()
this.blocks.push({
type: 'audio',
originalContentUrl: attrs.url,
duration: 1145,
})
} else if (type === 'face' && attrs.id) {
if (attrs.id.startsWith('s')) {
// https://developers.line.biz/en/reference/messaging-api/#sticker-message
await this.insertBlock()
this.blocks.push({
type: 'sticker',
packageId: attrs.id.split(':')[1],
stickerId: attrs.id.split(':')[2],
})
} else {
// https://developers.line.biz/en/reference/messaging-api/#text-message
this.emojis.push({
index: this.buffer.length,
productId: attrs.id.split(':')[1],
emojiId: attrs.id.split(':')[2],
})
this.buffer += '$'
}
} else if (type === 'author') {
this.sender.name = attrs.nickname
this.sender.iconUrl = attrs.avatar
} else if (type === 'message') {
// let childAuthor = h.select(children, 'author')
const sender = { ...this.sender }
await this.insertBlock()
await this.render(children)
await this.insertBlock()
if (this.sender.iconUrl || this.sender.name) {
this.sender = { ...sender }
}
}
}
}
Loading

0 comments on commit 08bb513

Please sign in to comment.