-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3354eea
commit 08bb513
Showing
10 changed files
with
1,845 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.