-
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.
Merge remote-tracking branch 'origin/line'
- Loading branch information
Showing
11 changed files
with
2,180 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,111 @@ | ||
import { Bot, Context, Logger, Quester, Schema, Universal } from '@satorijs/satori' | ||
import { HttpServer } from './http' | ||
import { Internal } from './types' | ||
import { LineMessageEncoder } from './message' | ||
|
||
const logger = new Logger('line') | ||
|
||
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) | ||
if (!ctx.root.config.selfUrl) { | ||
logger.warn('selfUrl is not set, some features may not work') | ||
} | ||
|
||
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, | ||
limit: 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 { | ||
token: string | ||
api: Quester.Config | ||
content: Quester.Config | ||
secret: string | ||
} | ||
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({ | ||
token: Schema.string().required(), | ||
secret: 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,49 @@ | ||
import { Adapter, Context, Logger } from '@satorijs/satori' | ||
import { LineBot } from './bot' | ||
import crypto from 'node:crypto' | ||
import { WebhookRequestBody } from './types' | ||
import { adaptSessions } from './utils' | ||
import internal from 'stream' | ||
|
||
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' | ||
}) | ||
bot.ctx.router.get('/line/assets/:self_id/:message_id', async (ctx) => { | ||
const messageId = ctx.params.message_id | ||
const selfId = ctx.params.self_id | ||
const localBot = this.bots.find((bot) => bot.selfId === selfId) | ||
if (!localBot) return ctx.status = 404 | ||
const resp = await localBot.contentHttp.axios<internal.Readable>(`/v2/bot/message/${messageId}/content`, { | ||
method: 'GET', | ||
responseType: 'stream', | ||
}) | ||
ctx.type = resp.headers['content-type'] | ||
ctx.set('cache-control', resp.headers['cache-control']) | ||
ctx.response.body = resp.data | ||
ctx.status = 200 | ||
}) | ||
} | ||
} |
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,103 @@ | ||
import { h, MessageEncoder } from '@satorijs/satori' | ||
import { LineBot } from './bot' | ||
import { Definitions } 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: Definitions.Message[] = [] | ||
block: Definitions.Message = null | ||
sender: Definitions.Sender = {} | ||
emojis: Definitions.Emoji[] = [] | ||
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.internal.pushMessage({ | ||
to: this.channelId, | ||
messages: this.blocks.slice(i, i + 5), | ||
}) | ||
} | ||
} | ||
|
||
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.