Skip to content

Commit 5e47911

Browse files
committed
feat(database): expose bot.getMessageList, support defaults
1 parent 0647a49 commit 5e47911

File tree

6 files changed

+200
-105
lines changed

6 files changed

+200
-105
lines changed

packages/chat/client/index.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
bg-gray-700 bg-op-0 hover:bg-op-100 transition"
5252
:class="{ successive: isSuccessive(index) }"
5353
@contextmenu.stop="triggerMessage($event, message)">
54-
<div class="quote" v-if="message.quote">
54+
<div class="quote" v-if="message.quote?.id">
5555
{{ message.quote }}
5656
</div>
5757
<template v-if="isSuccessive(index)">

packages/database/src/channel.ts

+71-52
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { Bot, Context, Logger, Session, Universal } from '@satorijs/core'
1+
import { Bot, Context, Logger, Session, Universal, omit } from '@satorijs/core'
22
import { Message } from './types'
3-
import { Span } from './span'
3+
import { MessageLike, Span } from './span'
44

5-
const logger = new Logger('sync')
5+
const logger = new Logger('message')
6+
logger.level = 3
67

78
export enum SyncStatus {
89
INIT,
910
READY,
1011
FAILED,
1112
}
1213

13-
type MessageLike = Message | { sid: bigint }
14-
1514
type LocateResult = [Span, MessageLike]
1615

1716
interface CollectResult {
@@ -28,13 +27,12 @@ export class SyncChannel {
2827

2928
private _initTask?: Promise<void>
3029

31-
constructor(public ctx: Context, public bot: Bot, public guildId: string, public data: Universal.Channel) {
32-
this._query = { platform: bot.platform, 'channel.id': data.id }
33-
bot.ctx.emit('satori/database/update')
30+
constructor(public ctx: Context, public bot: Bot, public channelId: string) {
31+
this._query = { platform: bot.platform, 'channel.id': channelId }
3432
}
3533

3634
private async init() {
37-
logger.debug('init channel %s %s %s', this.bot.platform, this.guildId, this.data.id)
35+
logger.debug('init channel %s %s', this.bot.platform, this.channelId)
3836
const data = await this.ctx.database
3937
.select('satori.message')
4038
.where({
@@ -76,10 +74,11 @@ export class SyncChannel {
7674
return left
7775
}
7876

79-
insert(data: Message[], options: Pick<Span, 'prev' | 'next' | 'prevTemp' | 'nextTemp'> = {}, forced: Span.PrevNext<boolean> = {}) {
77+
insert(data: Message[], options: Pick<Span, 'prev' | 'next' | 'prevTemp' | 'nextTemp'> = {}) {
8078
if (!data.length && !options.prev && !options.next) {
8179
throw new Error('unexpected empty span')
8280
}
81+
console.log('insert', data.length)
8382
const back: Span.Endpoint = [data.at(0)!.sid, data.at(0)!.id]
8483
const front: Span.Endpoint = [data.at(-1)!.sid, data.at(-1)!.id]
8584
const span = new Span(this, Span.Type.LOCAL, front, back, data)
@@ -91,82 +90,100 @@ export class SyncChannel {
9190
span.nextTemp = options.nextTemp
9291
span.link('after', options.next)
9392
span.merge('after')
94-
span.flush(forced)
93+
console.log('inserted', span.back[0], span.front[0], this._spans.length)
94+
span.flush()
9595
return span
9696
}
9797

9898
async queue(session: Session) {
9999
const prev = this.hasLatest ? this._spans[0] : undefined
100-
const message = Message.from(session.event.message!, session.platform, 'after', prev?.front[0])
100+
const message = Message.from(session.event.message!, session.platform, session.event, 'after', prev?.front[0])
101101
this.hasLatest = true
102-
this.insert([message], { prev }, { prev: true, next: true })
103-
}
104-
105-
getMessageList(id: string, dir?: Universal.Direction, limit?: number) {
106-
return this.bot.getMessageList(this.data.id, id, dir, limit, 'asc')
102+
this.insert([message], { prev })
107103
}
108104

109-
// TODO handle default limit
110-
async list(id: string, dir: Universal.Direction, limit: number) {
105+
async getMessageList(id?: string, dir: Universal.Direction = 'before', limit?: number, order: Universal.Order = 'asc') {
111106
await (this._initTask ||= this.init())
112-
const result = await this.locate(id, dir, limit)
113-
if (!result) return []
114-
const [span, message] = result
107+
logger.debug('message.list %s:%s', this.bot.platform, this.channelId, id, dir, limit, order)
108+
const location = await this.locate(id, dir, limit)
109+
if (!location) return { data: [] }
110+
const [span, message] = location
111+
logger.debug('location %s in [%s, %s]', message.sid, span.back[0], span.front[0])
112+
limit ??= this.ctx.get('satori.database')!.config.message.defaultLimit
115113
if (dir === 'around') limit = Math.floor(limit / 2) + 1
116-
const beforeTask = dir === 'after' ? Promise.resolve([]) : this.extend(span, message, limit, 'before')
117-
const afterTask = dir === 'before' ? Promise.resolve([]) : this.extend(span, message, limit, 'after')
114+
const beforeTask = dir === 'after' ? Promise.resolve([]) : this.extend(span, message, 'before', limit, id ? 1 : 0)
115+
const afterTask = dir === 'before' ? Promise.resolve([]) : this.extend(span, message, 'after', limit, 1)
118116
const [before, after] = await Promise.all([beforeTask, afterTask])
119-
if (dir === 'after') return after
120-
if (dir === 'before') return before
121-
return [...before.slice(0, -1), message, ...after.slice(1)]
117+
let result: Universal.TwoWayList<Universal.Message>
118+
// TODO: support hasEarliest and hasLatest
119+
if (dir === 'before') {
120+
result = { data: before, next: before.at(0)?.id, prev: before.at(0)?.id }
121+
} else if (dir === 'after') {
122+
result = { data: after, next: after.at(-1)?.id, prev: after.at(-1)?.id }
123+
} else {
124+
const data = [...before, message as Message, ...after]
125+
result = { data, next: data.at(-1)?.id, prev: data.at(0)?.id }
126+
}
127+
result.data = result.data.map((message) => omit(message, ['sid']))
128+
if (order === 'desc') result.data.reverse()
129+
return result
122130
}
123131

124-
collect(result: Universal.TwoWayList<Universal.Message>, dir: Span.Direction, data: Message[], index?: number): CollectResult {
132+
collect(result: Universal.TwoWayList<Universal.Message>, dir: Span.Direction, data: Message[], index: number): CollectResult {
125133
const w = Span.words[dir]
126-
index ??= dir === 'after' ? -1 : result.data.length
127-
for (let i = index + w.inc; i >= 0 && i < result.data.length; i += w.inc) {
134+
for (let i = index + w.unit; i >= 0 && i < result.data.length; i += w.unit) {
128135
const span = this._spans.find(span => span[w.back][1] === result.data[i].id)
129136
if (span) {
130-
const data = w.slice(result.data, i + w.inc)
137+
const data = w.slice(result.data, i + w.unit)
131138
if (data.length) {
132139
span[w.temp] = { [w.next]: result[w.next], data }
133140
}
134141
return { span }
135142
}
136-
data[w.push](Message.from(result.data[i], this.bot.platform, dir, data.at(w.last)?.sid))
143+
data[w.push](Message.from(result.data[i], this.bot.platform, undefined, dir, data.at(w.last)?.sid))
137144
}
138145
return { temp: { data: [], [w.next]: result[w.next] } }
139146
}
140147

141-
private async locate(id: string, dir: Universal.Direction, limit?: number): Promise<LocateResult | undefined> {
142-
// condition 1: message in memory
143-
for (const span of this._spans) {
144-
const message = span.data?.find(message => message.id === id)
145-
if (message) return [span, message]
146-
}
148+
private async locate(id?: string, dir: Universal.Direction = 'before', limit?: number): Promise<LocateResult | undefined> {
149+
if (id) {
150+
// condition 1: message in memory
151+
for (const span of this._spans) {
152+
const message = span.data?.find(message => message.id === id)
153+
if (message) return [span, message]
154+
}
147155

148-
// condition 2: message in database
149-
const data = await this.ctx.database
150-
.select('satori.message')
151-
.where({ ...this._query, id })
152-
.execute()
153-
if (data[0]) {
154-
const { sid } = data[0]
155-
const span = this._spans[this.binarySearch(sid)]
156-
if (!span || span.back[0] > sid || span.front[0] < sid) throw new Error('malformed sync span')
157-
return [span, data[0]]
156+
// condition 2: message in database
157+
const data = await this.ctx.database
158+
.select('satori.message')
159+
.where({ ...this._query, id })
160+
.execute()
161+
if (data[0]) {
162+
const { sid } = data[0]
163+
const span = this._spans[this.binarySearch(sid)]
164+
if (!span || span.back[0] > sid || span.front[0] < sid) throw new Error('malformed sync span')
165+
return [span, data[0]]
166+
}
167+
} else if (this.hasLatest) {
168+
return [this._spans[0], { sid: this._spans[0].front[0] }]
158169
}
159170

160171
// condition 3: message not cached, request from adapter
161172
let span: Span
162173
let message: MessageLike
163174
let index: number | undefined
164-
const result = await this.getMessageList(id, dir, limit)
175+
const data: Message[] = []
176+
const result = await this.bot.self.getMessageList(this.channelId, id, dir, limit)
177+
console.log('raw:', result.data.length)
165178
if (dir === 'around') {
166179
index = result.data.findIndex(item => item.id === id)
167180
if (index === -1) throw new Error('malformed message list')
168181
message = Message.from(result.data[index], this.bot.platform)
169182
data.push(message as Message)
183+
} else if (dir === 'before') {
184+
index = result.data.length
185+
} else {
186+
index = -1
170187
}
171188

172189
const { span: prev, temp: prevTemp } = this.collect(result, 'before', data, index)
@@ -191,12 +208,12 @@ export class SyncChannel {
191208
return [span, message!]
192209
}
193210

194-
private async extend(span: Span, message: MessageLike, limit: number, dir: Span.Direction) {
211+
private async extend(span: Span, message: MessageLike, dir: Span.Direction, limit: number, exclusive: number) {
195212
const buffer: Message[] = []
196213
const w = Span.words[dir]
197214

198215
while (true) {
199-
const data = await span.collect(message, dir, limit - buffer.length)
216+
const data = await span.collect(message, dir, limit - buffer.length, exclusive)
200217
buffer[w.push](...data)
201218
if (buffer.length >= limit) {
202219
delete span[w.temp]
@@ -206,10 +223,11 @@ export class SyncChannel {
206223
let result = span[w.temp]
207224
if (result) {
208225
let i = dir === 'before' ? result.data.length - 1 : 0
209-
for (; i >= 0 && i < result.data.length; i += w.inc) {
226+
for (; i >= 0 && i < result.data.length; i += w.unit) {
210227
if (!data.some(item => item.id === result!.data[i].id)) break
211228
}
212229
result.data = w.slice(result.data, i)
230+
// inherit next?
213231
if (!result.data.length) result = undefined
214232
delete span[w.temp]
215233
}
@@ -219,6 +237,7 @@ export class SyncChannel {
219237

220238
span = next
221239
message = { sid: span[w.back][0] }
240+
exclusive = 0
222241
}
223242

224243
if (dir === 'before') {

packages/database/src/guild.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export class SyncGuild {
44
public members?: Universal.List<Universal.GuildMember>
55

66
constructor(public bot: Bot, public data: Universal.Guild) {
7-
bot.ctx.emit('satori/database/update')
87
}
98

109
async getMembers() {

packages/database/src/index.ts

+59-16
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { SyncGuild } from './guild'
55

66
export * from './types'
77

8-
declare module '@satorijs/core' {
9-
interface Satori {
10-
database: SatoriDatabase
8+
declare module 'cordis' {
9+
interface Context {
10+
'satori.database': SatoriDatabase
1111
}
1212
}
1313

14-
declare module 'cordis' {
15-
interface Events {
16-
'satori/database/update'(): void
14+
declare module '@satorijs/core' {
15+
interface Satori {
16+
database: SatoriDatabase
1717
}
1818
}
1919

@@ -30,13 +30,31 @@ class SatoriDatabase extends Service<SatoriDatabase.Config, Context> {
3030
constructor(ctx: Context, public config: SatoriDatabase.Config) {
3131
super(ctx, 'satori.database', true)
3232

33-
// TODO bot mixin
34-
// ctx.mixin('satori.database', {
35-
// 'createMessage': 'bot.createMessage',
36-
// 'getMessage': 'bot.getMessage',
37-
// 'getMessageList': 'bot.getMessageList',
33+
const self = this
34+
35+
// ctx.accessor('bot.getGuildList', {
36+
// get: () => async function (this: Bot) {
37+
// const data = await ctx.database.get('satori.guild', {
38+
// logins: {
39+
// $some: {
40+
// platform: this.platform,
41+
// 'user.id': this.user.id,
42+
// },
43+
// },
44+
// })
45+
// if (data.length) return data
46+
// return data
47+
// },
3848
// })
3949

50+
ctx.accessor('bot.getMessageList', {
51+
get: () => async function (this: Bot, channelId: string, id: string, dir?: Universal.Direction, limit?: number, order?: Universal.Order) {
52+
const key = this.platform + '/' + channelId
53+
self._channels[key] ||= new SyncChannel(ctx, this, channelId)
54+
return await self._channels[key].getMessageList(id, dir, limit, order)
55+
},
56+
})
57+
4058
ctx.model.extend('satori.message', {
4159
'uid': 'unsigned(8)',
4260
'sid': 'bigint', // int64
@@ -55,6 +73,10 @@ class SatoriDatabase extends Service<SatoriDatabase.Config, Context> {
5573
}, {
5674
primary: 'uid',
5775
autoInc: true,
76+
unique: [
77+
['id', 'channel.id', 'platform'],
78+
['sid', 'channel.id', 'platform'],
79+
],
5880
})
5981

6082
ctx.model.extend('satori.user', {
@@ -71,6 +93,7 @@ class SatoriDatabase extends Service<SatoriDatabase.Config, Context> {
7193
'id': 'char(255)',
7294
'platform': 'char(255)',
7395
'name': 'char(255)',
96+
'avatar': 'char(255)',
7497
}, {
7598
primary: ['id', 'platform'],
7699
})
@@ -82,14 +105,26 @@ class SatoriDatabase extends Service<SatoriDatabase.Config, Context> {
82105
}, {
83106
primary: ['id', 'platform'],
84107
})
108+
109+
ctx.model.extend('satori.login', {
110+
'platform': 'char(255)',
111+
'user.id': 'char(255)',
112+
'guilds': {
113+
type: 'manyToMany',
114+
table: 'satori.guild',
115+
target: 'logins',
116+
},
117+
}, {
118+
primary: ['platform', 'user.id'],
119+
})
85120
}
86121

87122
async start() {
88123
this.ctx.on('message', (session) => {
89-
const { platform, guildId, channelId } = session
124+
const { platform, channelId } = session
90125
if (session.bot.hidden) return
91-
const key = platform + '/' + guildId + '/' + channelId
92-
this._channels[key] ||= new SyncChannel(this.ctx, session.bot, session.guildId, session.event.channel!)
126+
const key = platform + '/' + channelId
127+
this._channels[key] ||= new SyncChannel(this.ctx, session.bot, session.channelId)
93128
if (this._channels[key].bot === session.bot) {
94129
this._channels[key].queue(session)
95130
}
@@ -163,9 +198,17 @@ class SatoriDatabase extends Service<SatoriDatabase.Config, Context> {
163198
}
164199

165200
namespace SatoriDatabase {
166-
export interface Config {}
201+
export interface Config {
202+
message: {
203+
defaultLimit: number
204+
}
205+
}
167206

168-
export const Config: Schema<Config> = Schema.object({})
207+
export const Config: Schema<Config> = Schema.object({
208+
message: Schema.object({
209+
defaultLimit: Schema.natural().default(50),
210+
}),
211+
})
169212
}
170213

171214
export default SatoriDatabase

0 commit comments

Comments
 (0)