From a07a9c2152183216f84705e50766153a9cb9d7bd Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:25:39 +0800 Subject: [PATCH 01/50] chore: update dependencies --- package.json | 71 ++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 0b713fb..03e9590 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,38 @@ { - "name": "@root/satori-im", - "private": true, - "type": "module", - "version": "1.0.0", - "workspaces": [ - "packages/*" - ], - "license": "MIT", - "scripts": { - "build": "yakumo build", - "lint": "eslint --cache", - "test": "yakumo mocha -r esbuild-register -t 10000", - "test:text": "shx rm -rf coverage && c8 -r text yarn test", - "test:json": "shx rm -rf coverage && c8 -r json yarn test", - "test:html": "shx rm -rf coverage && c8 -r html yarn test" - }, - "devDependencies": { - "@cordisjs/eslint-config": "^1.1.1", - "@types/chai": "^4.3.14", - "@types/mocha": "^9.1.1", - "@types/node": "^20.11.30", - "c8": "^7.14.0", - "chai": "^4.4.1", - "esbuild": "^0.18.20", - "esbuild-register": "^3.5.0", - "eslint": "^8.57.0", - "mocha": "^9.2.2", - "shx": "^0.3.4", - "typescript": "^5.4.3", - "yakumo": "^1.0.0-beta.16", - "yakumo-esbuild": "^1.0.0-beta.6", - "yakumo-mocha": "^1.0.0-beta.2", - "yakumo-tsc": "^1.0.0-beta.4", - "yml-register": "^1.2.5" + "name": "@root/satori-im", + "private": true, + "type": "module", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ], + "license": "MIT", + "scripts": { + "build": "yakumo build", + "lint": "eslint --cache", + "test": "yakumo mocha -r esbuild-register -t 10000", + "test:text": "shx rm -rf coverage && c8 -r text yarn test", + "test:json": "shx rm -rf coverage && c8 -r json yarn test", + "test:html": "shx rm -rf coverage && c8 -r html yarn test" + }, + "devDependencies": { + "@cordisjs/eslint-config": "^1.1.1", + "@types/chai": "^4.3.14", + "@types/mocha": "^9.1.1", + "@types/node": "^20.11.30", + "c8": "^7.14.0", + "chai": "^4.4.1", + "esbuild": "^0.18.20", + "esbuild-register": "^3.5.0", + "eslint": "^8.57.0", + "mocha": "^9.2.2", + "shx": "^0.3.4", + "typescript": "^5.4.3", + "yakumo": "^1.0.0-beta.16", + "yakumo-esbuild": "^1.0.0-beta.6", + "yakumo-mocha": "^1.0.0-beta.2", + "yakumo-tsc": "^1.0.0-beta.4", + "yml-register": "^1.2.5" + } } -} + \ No newline at end of file From 414cd3beb7a96db782c8c91699a3aa69626e650f Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:26:15 +0800 Subject: [PATCH 02/50] chore: setup core pack --- packages/core/package.json | 57 +++++++++++++++++++++++++++++++++++++ packages/core/tsconfig.json | 10 +++++++ 2 files changed, 67 insertions(+) create mode 100644 packages/core/package.json create mode 100644 packages/core/tsconfig.json diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..2a8476c --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,57 @@ +{ + "name": "@satorijs/plugin-im", + "description": "Background for Satori IM", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/im" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "chat", + "im", + "database" + ], + "cordis": { + "service": { + "required": [ + "database-mysql" + ], + "implements": [ + "im.data" + ] + } + }, + "peerDependencies": { + "@satorijs/core": "^4.1.1" + }, + "devDependencies": { + "@cordisjs/plugin-webui": "^0.1.7", + "@satorijs/core": "^4.1.1", + "@types/uuid": "^10", + "uuid": "^10.0.0" + }, + "dependencies": { + "@minatojs/driver-mysql": "^3.4.1", + "cordis": "^3.17.4", + "cosmokit": "^1.6.2", + "minato": "^3.4.3" + } + } + \ No newline at end of file diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..e193a11 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + }, + "include": [ + "src", + ], +} \ No newline at end of file From 7696373325ceb2566bc96ccffb1e96af407aa81e Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:35:46 +0800 Subject: [PATCH 03/50] feat(models): add data models --- packages/core/src/database.ts | 252 ++++++++++++++++++++++++++++ packages/core/src/index.ts | 63 +++++++ packages/core/src/models/channel.ts | 36 ++++ packages/core/src/models/friend.ts | 53 ++++++ packages/core/src/models/guild.ts | 75 +++++++++ packages/core/src/models/index.ts | 4 + packages/core/src/models/message.ts | 0 packages/core/src/models/user.ts | 37 ++++ 8 files changed, 520 insertions(+) create mode 100644 packages/core/src/database.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/models/channel.ts create mode 100644 packages/core/src/models/friend.ts create mode 100644 packages/core/src/models/guild.ts create mode 100644 packages/core/src/models/index.ts create mode 100644 packages/core/src/models/message.ts create mode 100644 packages/core/src/models/user.ts diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts new file mode 100644 index 0000000..af6d280 --- /dev/null +++ b/packages/core/src/database.ts @@ -0,0 +1,252 @@ +import { Context } from '@satorijs/core' + +export default class ImDatabase { + constructor(public ctx: Context) { + ctx.model.extend( + 'satori-im.user', + { + id: 'char(255)', + name: { + type: 'char', + length: 255, + nullable: false, + }, + nick: 'char(255)', + avatar: 'char(255)', + password: { + type: 'char', + length: 255, + nullable: false, + }, + }, + { + primary: ['id'], + unique: ['name'], + } + ) + ctx.model.extend( + 'satori-im.user.settings', + { + origin: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'settings.user', + }, + target: { + type: 'manyToOne', + table: 'satori-im.user', + }, + level: { + type: 'unsigned', + length: 1, + initial: 2, + }, + }, + { primary: ['origin', 'target'] } + ) + ctx.model.extend( + 'satori-im.friend', + { + origin: { + type: 'manyToOne', + table: 'satori-im.user', + }, + target: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'friends', + }, + group: 'char(255)', + pinned: { + type: 'boolean', + initial: false, + }, + nick: 'char(255)', + }, + { + primary: ['origin', 'target'], + } + ) + ctx.model.extend( + 'satori-im.guild', + { + id: 'char(255)', + name: { + type: 'string', + length: 255, + nullable: false, + }, + avatar: 'char(255)', + }, + { + primary: ['id'], + } + ) + ctx.model.extend( + 'satori-im.guild.settings', + { + guild: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'settings.guild', + }, + user: { + type: 'manyToOne', + table: 'satori-im.guild', + }, + group: 'char(255)', + pinned: 'boolean', + }, + { + primary: ['guild', 'user'], + } + ) + ctx.model.extend( + 'satori-im.member', + { + guild: { + type: 'manyToOne', + table: 'satori-im.guild', + target: 'members', + }, + user: { + type: 'manyToOne', + table: 'satori-im.user', + }, + name: 'char(255)', // name of member identity + }, + { + primary: ['guild', 'user'], + } + ) + ctx.model.extend( + 'satori-im.role', + { + id: 'char(255)', + user: { + type: 'manyToMany', + table: 'satori-im.user', + target: 'roles', + }, + // guild: { + // type: 'manyToOne', + // table: 'satori-im.guild', + // target: 'roles', + // nullable: false, + // }, + 'guild.id': 'char(255)', + name: { + type: 'string', + length: 255, + nullable: false, + }, + color: 'integer', + position: 'integer', + permissions: 'bigint', + hoist: 'boolean', + mentionable: 'boolean', + }, + { + primary: ['id'], + unique: [['guild.id', 'name']], + } + ) + ctx.model.extend( + 'satori-im.message.test', + { + id: 'char(255)', + 'user.id': 'char(255)', + 'channel.id': 'char(255)', + 'guild.id': 'char(255)', + 'quote.id': 'char(255)', + content: 'text', + createdAt: 'unsigned(8)', + updatedAt: 'unsigned(8)', + }, + { + primary: ['id'], + } + ) + ctx.model.extend( + 'satori-im.message.settings', + { + message: { + type: 'manyToOne', + table: 'satori-im.message.test', + }, + user: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'settings.message', + }, + }, + { primary: ['message', 'user'] } + ) + ctx.model.extend( + 'satori-im.channel', + { + id: 'char(255)', + name: 'char(255)', + type: 'unsigned(1)', + parentId: 'char(255)', + }, + { + primary: ['id'], + } + ) + ctx.model.extend( + 'satori-im.channel.settings', + { + user: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'settings.channel', + }, + channel: { + type: 'manyToOne', + table: 'satori-im.channel', + }, + pinned: { + type: 'boolean', + initial: false, + }, + level: { + type: 'unsigned', + length: 1, + initial: 0, + }, + lastRead: 'char(255)', + }, + { + primary: ['channel', 'user'], + } + ) + ctx.model.extend( + 'satori-im.login', + { + user: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'logins', + }, + status: 'unsigned(1)', + updateAt: 'unsigned(8)', + }, + { + primary: ['user'], + } + ) + ctx.model.extend('satori-im.notification', { + self: { + type: 'manyToOne', + table: 'satori-im.user', + }, + user: { + type: 'manyToOne', + table: 'satori-im.user', + }, + 'guild.id': 'char(255)', + type: 'unsigned(0)', + }) + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..572b937 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,63 @@ +import {} from 'minato' +import {} from '@cordisjs/plugin-webui' +import { Context } from '@satorijs/core' + +import ImDatabase from './database' +import { ImDataService } from './data' +import { ImEventService } from './notify' +import { ImLoginService } from './login' +import { Channel, Friend, Guild, Member, Message, Role, User } from './types' + +export * as ImTypes from './types' +export * from './utils' + +declare module '@cordisjs/plugin-webui' { + interface Events { + 'im/v1/user/fetch'(uid: string): Promise + 'im/v1/user/update'(data: { uid: string } & Partial): Promise + 'im/v1/user/remove'(uid: string): Promise + + 'im/v1/friend/fetch'(uid: string, target: string): Promise + 'im/v1/friend/fetch-all'(uid: string): Promise> + 'im/v1/friend/remove'(uid: string, target: string): Promise + + 'im/v1/guild/fetch'(gid: string): Promise> + 'im/v1/guild/fetch-all'(uid: string): Promise> + 'im/v1/guild/create'(data: { name: string }): Promise + 'im/v1/guild/remove'(gid: string): Promise + + 'im/guild-member/fetch'( + uid: string, + gid: string + ): Promise & { role: Role }> + 'im/v1/guild-member/fetch-all'( + gid: string + ): Promise & { role: Role }>> + + 'im/v1/guild-role/fetch'(data: { gid: string; rid: string }): Promise + 'im/v1/guild-role/fetch-all'(gid: string): Promise> + 'im/v1/guild-role/create'(uid: string, gid: string): Promise + + 'im/v1/channel/fetch'(cid: string): Promise + 'im/v1/channel/create'(gid: string, data: { name: string }): Promise + 'im/v1/channel/remove'(cid: string): Promise + + 'im/v1/message/fetch'(id: string): Promise + 'im/v1/message/fetch-all'(data: { cid: string; uid?: string }): Promise> + } +} + +declare module 'cordis' { + interface Context { + 'im.data': ImDataService + 'im.event': ImEventService + 'im.login': ImLoginService + } +} + +export function apply(ctx: Context) { + new ImDatabase(ctx) + ctx.plugin(ImDataService) + ctx.plugin(ImLoginService) + ctx.plugin(ImEventService) +} diff --git a/packages/core/src/models/channel.ts b/packages/core/src/models/channel.ts new file mode 100644 index 0000000..aa8d338 --- /dev/null +++ b/packages/core/src/models/channel.ts @@ -0,0 +1,36 @@ +import { Context, Universal } from '@satorijs/core' +import { Channel } from '../types' +import { genId } from '../utils' + +export class ChannelData { + constructor(public ctx: Context) { + ctx.webui.addListener('im/v1/channel/fetch', this.fetch) + ctx.webui.addListener('im/v1/channel/create', this.create) + } + + async fetch(cid: string): Promise { + const result = await this.ctx.database.get('satori-im.channel', { + id: cid, + }) + return result[0] + } + + async create(gid: string, data: { name: string }): Promise { + return await this.ctx.database.create('satori-im.channel', { + id: genId(), + name: data.name, + type: Universal.Channel.Type.TEXT, + parentId: gid, + }) + } + + async _update(cid: string, data: { name: string }): Promise { + const result = await this.ctx.database.set('satori-im.channel', cid, { + name: data.name, + }) + } + + async _softDel(cid: string): Promise { + const result = await this.ctx.database.remove('satori-im.channel', cid) + } +} diff --git a/packages/core/src/models/friend.ts b/packages/core/src/models/friend.ts new file mode 100644 index 0000000..e5741c7 --- /dev/null +++ b/packages/core/src/models/friend.ts @@ -0,0 +1,53 @@ +import { $ } from 'minato' +import { Context } from '@satorijs/core' +import { Friend } from '../types' + +export class FriendData { + constructor(public ctx: Context) { + ctx.webui.addListener('im/v1/friend/fetch', this.fetch) + ctx.webui.addListener('im/v1/friend/fetch-all', this.fetchAll) + ctx.webui.addListener('im/v1/friend/remove', this.hardDel) + } + + async fetch(uid: string, target: string): Promise { + const result = await this.ctx.database.get('satori-im.friend', (row) => + $.or( + $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), + $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) + ) + ) + return result[0] + } + + async fetchAll(uid: string): Promise> { + const result = await this.ctx.database.get('satori-im.friend', (row) => + $.or($.eq(row.origin.id, uid), $.eq(row.target.id, uid)) + ) + return result + } + + async _update(uid: string, target: string, data: Partial): Promise { + const result = await this.ctx.database.set( + 'satori-im.friend', + (row) => + $.or( + $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), + $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) + ), + { + pinned: data.pinned, + nick: data.nick, + group: data.group, + } + ) + } + + async hardDel(uid: string, target: string): Promise { + const result = await this.ctx.database.remove('satori-im.friend', (row) => + $.or( + $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), + $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) + ) + ) + } +} diff --git a/packages/core/src/models/guild.ts b/packages/core/src/models/guild.ts new file mode 100644 index 0000000..d93d338 --- /dev/null +++ b/packages/core/src/models/guild.ts @@ -0,0 +1,75 @@ +import { $ } from 'minato' +import { Context, Universal } from '@satorijs/core' +import { Guild, Member, Role } from '../types' +import { genId } from '../utils' + +declare module '@cordisjs/plugin-webui' { + interface Events {} +} + +export class GuildData { + public cache?: Universal.List + + constructor(public ctx: Context) { + ctx.webui.addListener('im/v1/guild/fetch', this.fetch) + ctx.webui.addListener('im/v1/guild/fetch-all', this.list) + ctx.webui.addListener('im/v1/guild/create', this.create) + + ctx.webui.addListener('im/v1/guild-member/fetch-all', this.Member.list) + } + + async fetch(gid: string): Promise> { + return this.ctx.database.get('satori-im.guild', gid, ['id', 'name', 'avatar']) + } + + async list(uid: string): Promise> { + const result = await this.ctx.database.get('satori-im.member', { user: { id: uid } }, ['guild']) + return result.map((data) => { + return data.guild + }) + } + + async create(data: { name: string }): Promise { + const result = await this.ctx.database.create('satori-im.guild', { + id: genId(), + name: data.name, + }) + return result + } + + // TODO: permission check. + async _update(gid: string, data: Partial): Promise { + await this.ctx.database.set('satori-im.guild', gid, { + name: data.name, + }) + } + + async _softDel(gid: string): Promise { + await this.ctx.database.remove('satori-im.guild', gid) + } + + public Member = { + fetch: async (uid: string, gid: string): Promise => { + const result = await this.ctx.database.get('satori-im.member', (row) => + $.and($.eq(row.user.id, uid), $.eq(row.guild.id, gid)) + ) + return result[0] + }, + + list: async (gid: string): Promise & { role: Role }>> => { + const result = await this.ctx.database + .join(['satori-im.member', 'satori-im.role'], (member, role) => + $.eq(member['user.id'], role.user) + ) + .where((row) => $.eq('guild.id', gid)) + .orderBy('satori-im.member.name', 'asc') + .project({ + user: (row) => row['satori-im.member'].user, + role: (row) => row['satori-im.role'], + name: (row) => row['satori-im.member'].name, + }) + .execute() + return result + }, + } +} diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts new file mode 100644 index 0000000..d79059e --- /dev/null +++ b/packages/core/src/models/index.ts @@ -0,0 +1,4 @@ +export * from './channel' +export * from './friend' +export * from './user' +export * from './guild' diff --git a/packages/core/src/models/message.ts b/packages/core/src/models/message.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/src/models/user.ts b/packages/core/src/models/user.ts new file mode 100644 index 0000000..8194bfb --- /dev/null +++ b/packages/core/src/models/user.ts @@ -0,0 +1,37 @@ +import { Context } from '@satorijs/core' +import { User } from '../types' + +export class UserData { + constructor(public ctx: Context) { + ctx.webui.addListener('im/v1/user/fetch', this.fetch) + ctx.webui.addListener('im/v1/user/update', this.update) + ctx.webui.addListener('im/v1/user/remove', this.softDel) + } + + async fetch(uid: string): Promise { + const result = await this.ctx.database.get('satori-im.user', uid, [ + 'id', + 'name', + 'avatar', + 'nick', + ]) + return result[0] + } + + async update(data: { uid: string } & Partial): Promise { + const result = await this.ctx.database.set('satori-im.user', data.uid, (row) => ({ + nick: data.nick, + avatar: data.avatar, + })) + if (!result.modified) { + throw new Error() + } + } + + async softDel(uid: string): Promise {} + + // TODO: waiting for implementation. + async search(keyword: string): Promise> { + return this.ctx.database.get('satori-im.user', {}) + } +} From 88ddcff49332f45b5e20f93d47ec602b22422df0 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:36:04 +0800 Subject: [PATCH 04/50] feat(core): add im api service --- packages/core/src/data.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/core/src/data.ts diff --git a/packages/core/src/data.ts b/packages/core/src/data.ts new file mode 100644 index 0000000..646bbce --- /dev/null +++ b/packages/core/src/data.ts @@ -0,0 +1,9 @@ +import { Context, Service } from '@satorijs/core' + +export class ImDataService extends Service { + static inject = ['model', 'database', 'webui'] + + constructor(public ctx: Context) { + super(ctx, 'im.data', true) + } +} From fc0a1d26551c2d7b4760f2f2a632c3595e58f1eb Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:36:30 +0800 Subject: [PATCH 05/50] feat(core): add im login service --- packages/core/src/login.ts | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/core/src/login.ts diff --git a/packages/core/src/login.ts b/packages/core/src/login.ts new file mode 100644 index 0000000..f206e9e --- /dev/null +++ b/packages/core/src/login.ts @@ -0,0 +1,58 @@ +import { Context, Dict, Service, Universal } from '@satorijs/core' +import { Login, User } from './types' +import { UserData } from './models' +import { genId, CharValidator } from './utils' + +declare module '@cordisjs/plugin-webui' { + interface Events { + 'im/v1/user/login-add'(data: { name: string; password: string }): Promise + 'im/v1/user/login-change'(data: { uid: string; status: number }): Promise + 'im/v1/user/login-remove'(uid: string): Promise + 'im/v1/user/register'(data: { name: string; password: string }): Promise + } +} + +// TODO: password crypt? +export class ImLoginService extends Service { + static inject = ['model', 'database', 'webui'] + public cache: Dict = {} + + constructor(public ctx: Context) { + super(ctx, 'im.login', true) + + ctx.webui.addListener('im/v1/user/login-add', this.accessibility) + ctx.webui.addListener('im/v1/user/login-change', this.sync) + ctx.webui.addListener('im/v1/user/login-remove', this.logout) + ctx.webui.addListener('im/v1/user/register', this.register) + // ctx.on('dispose', this._save()) + } + + validate = new CharValidator().validate + + accessibility = async (data: { name: string; password: string }): Promise => { + const result = await this.ctx.database.get('satori-im.user', data) + if (result.length === 0) { + throw Error() + } + this.cache[result[0].id] = { + user: result[0], + status: Universal.Status.ONLINE, + updateAt: new Date().getTime(), + features: [], + proxyUrls: [], + } + return result[0] + } + + register = async (data: { name: string; password: string }): Promise => { + const user = { id: genId(), ...data } + const { password: _, ...result } = await this.ctx.database.create('satori-im.user', user) + return result + } + + sync = async (data: { uid: string; status: number }) => {} + + async logout(uid: string): Promise { + delete this.cache[uid] + } +} From 176bdef1e22441f141c56a932892c532cde9f191 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:36:54 +0800 Subject: [PATCH 06/50] feat(core): add im event service --- packages/core/src/notify.ts | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/core/src/notify.ts diff --git a/packages/core/src/notify.ts b/packages/core/src/notify.ts new file mode 100644 index 0000000..a2bd410 --- /dev/null +++ b/packages/core/src/notify.ts @@ -0,0 +1,63 @@ +import { Context, Dict, Service } from '@satorijs/core' +import { FriendData, GuildData } from './models' +import { Event, Notification } from './types' + +declare module '@cordisjs/plugin-webui' { + interface Events { + 'im/v1/invitation/friend'(uid: string, target: string, content?: string): Promise + 'im/v1/invitation/member'( + uid: string, + gid: string, + target: string, + content?: string + ): Promise + 'im/v1/invitation/reply'(uid: string, originId: string, reply: boolean): Promise + } +} + +export class ImEventService extends Service { + static inject = ['model', 'database', 'webui'] + private _chans: { + notification: Dict> + event: Dict + } + + constructor(public ctx: Context) { + super(ctx, 'im.event', true) + this._chans = { notification: {}, event: {} } + + ctx.webui.addListener('im/v1/invitation/friend', this.request) + ctx.webui.addListener('im/v1/invitation/member', this.request) + ctx.webui.addListener('im/v1/invitation/reply', this.reply) + } + + request = async (uid: string, target: string, gid?: string, content?: string) => { + const invitation = { self: { id: uid }, user: { id: target }, guild: { id: gid } } + // this._notificationChan[uid].push(...this.notifications[uid]) + } + + reply = async (uid: string, origin: string, reply: boolean, gid?: string) => { + const invitations = this._chans.notification[origin] + if (!invitations) { + throw Error() + } + const index = invitations.findIndex( + (value) => (gid === undefined || value.guild.id === gid) && value.user.id === uid + ) + if (index === -1) { + throw Error() + } + if (reply === true) { + gid + ? await this.ctx.database.create('satori-im.member', { + user: { id: uid }, + guild: { id: gid }, + }) + : await this.ctx.database.create('satori-im.friend', { + origin: { id: origin }, + target: { id: uid }, + }) // TODO + } + invitations.splice(index, 1) + } +} From 63b566d183ca35468490f3844656f83b230a6098 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:39:08 +0800 Subject: [PATCH 07/50] feat(models): add model typing --- packages/core/src/types.ts | 137 +++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 packages/core/src/types.ts diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..1296f22 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,137 @@ +import { Universal } from '@satorijs/core' + +declare module 'minato' { + interface Tables { + 'satori-im.channel.settings': Channel.Settings + 'satori-im.channel': Channel + 'satori-im.friend': Friend + 'satori-im.guild.settings': Guild.Settings + 'satori-im.guild': Guild + 'satori-im.login': Login + 'satori-im.member': Member + 'satori-im.message.settings': Message.Settings + 'satori-im.message.test': Message + 'satori-im.role': Role + 'satori-im.user.settings': User.Settings + 'satori-im.user': User + 'satori-im.notification': Notification + } +} + +declare module '@satorijs/protocol' { + interface Message { + sid?: bigint + } +} + +export interface Login extends Universal.Login { + user: User + updateAt: number +} + +export interface Notification { + self: User + type: Notification.Types + user: User + guild: Guild + content?: string +} + +export namespace Notification { + export enum Types { + NOT = 0, + REQ = 1, + } +} + +export type Event = Universal.Event + +export interface User extends Universal.User { + password?: string +} + +export namespace User { + export interface Settings { + origin: User + target: User + level: NotifyLevels + } +} + +export interface Friend { + id: string + origin: User + target: User + group: string + pinned: boolean + nick?: string +} + +export interface Guild extends Universal.Guild {} + +export namespace Guild { + export interface Settings { + guild: Guild + user: User + group: string + pinned: boolean + } +} + +export interface Member extends Universal.GuildMember { + guild: Guild + user: User +} + +export interface Role extends Universal.GuildRole { + user: User + guild: Guild +} + +export interface Channel extends Universal.Channel {} + +export namespace Channel { + export interface Settings { + user: User + channel: Channel + level: NotifyLevels + nick?: string + pinned: boolean + lastRead: string + } +} + +export interface Message extends Universal.Message { + sid?: bigint // message sync id. + // type: Message.Type + flag: number +} + +export namespace Message { + export interface Settings { + message: Message + user: User + } + + export enum Flag { + FRONT = 1, + BACK = 2, + FINAL = 4, + } + + /** @deprecated */ + export enum Types { + PLAIN = 1, + } + + // 序列化 + + // 创建 Message +} + +export enum NotifyLevels { + BLOCKED = 0, + SILENT = 1, + NORMAL = 2, + IMPORTANT = 3, +} From 95bd5c9b0a99f112a91c38aee92fcfaf32d50b08 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:41:03 +0800 Subject: [PATCH 08/50] feat(webui): add components --- packages/im/client/components/aside/aside.vue | 22 ++ packages/im/client/components/aside/index.ts | 6 + packages/im/client/components/index.ts | 20 ++ .../im/client/components/list/chat-list.vue | 254 ++++++++++++++++++ .../im/client/components/list/friend-item.vue | 6 + .../im/client/components/list/guild-item.vue | 6 + packages/im/client/components/list/index.ts | 8 + .../client/components/list/message-item.vue | 60 +++++ .../client/components/list/message-list.vue | 52 ++++ .../client/components/list/session-item.vue | 0 packages/im/client/components/role/avatar.vue | 44 +++ packages/im/client/components/role/index.ts | 10 + .../im/client/components/role/login-form.vue | 116 ++++++++ .../im/client/components/role/role-tag.vue | 9 + packages/im/client/components/tab/index.ts | 6 + .../im/client/components/tab/tab-item.vue | 39 +++ packages/im/client/components/tab/tab.vue | 52 ++++ .../im/client/components/windowed/index.ts | 6 + .../components/windowed/message-window.vue | 51 ++++ packages/im/client/index.ts | 36 ++- packages/im/client/index.vue | 43 +++ packages/im/client/shared/index.ts | 14 + 22 files changed, 858 insertions(+), 2 deletions(-) create mode 100644 packages/im/client/components/aside/aside.vue create mode 100644 packages/im/client/components/aside/index.ts create mode 100644 packages/im/client/components/index.ts create mode 100644 packages/im/client/components/list/chat-list.vue create mode 100644 packages/im/client/components/list/friend-item.vue create mode 100644 packages/im/client/components/list/guild-item.vue create mode 100644 packages/im/client/components/list/index.ts create mode 100644 packages/im/client/components/list/message-item.vue create mode 100644 packages/im/client/components/list/message-list.vue create mode 100644 packages/im/client/components/list/session-item.vue create mode 100644 packages/im/client/components/role/avatar.vue create mode 100644 packages/im/client/components/role/index.ts create mode 100644 packages/im/client/components/role/login-form.vue create mode 100644 packages/im/client/components/role/role-tag.vue create mode 100644 packages/im/client/components/tab/index.ts create mode 100644 packages/im/client/components/tab/tab-item.vue create mode 100644 packages/im/client/components/tab/tab.vue create mode 100644 packages/im/client/components/windowed/index.ts create mode 100644 packages/im/client/components/windowed/message-window.vue create mode 100644 packages/im/client/index.vue create mode 100644 packages/im/client/shared/index.ts diff --git a/packages/im/client/components/aside/aside.vue b/packages/im/client/components/aside/aside.vue new file mode 100644 index 0000000..f9da90d --- /dev/null +++ b/packages/im/client/components/aside/aside.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/im/client/components/aside/index.ts b/packages/im/client/components/aside/index.ts new file mode 100644 index 0000000..601bdbf --- /dev/null +++ b/packages/im/client/components/aside/index.ts @@ -0,0 +1,6 @@ +import { App } from 'vue' +import Aside from './aside.vue' + +export default function (app: App) { + app.component('k-im-aside', Aside) +} diff --git a/packages/im/client/components/index.ts b/packages/im/client/components/index.ts new file mode 100644 index 0000000..5009bc2 --- /dev/null +++ b/packages/im/client/components/index.ts @@ -0,0 +1,20 @@ +import { App } from 'vue' +import aside from './aside' +import role from './role' +import list from './list' +import tab from './tab' +import windowed from './windowed' + +export * from './aside' +export * from './role' +export * from './list' +export * from './tab' +export * from './windowed' + +export default function (app: App) { + app.use(aside) + app.use(list) + app.use(tab) + app.use(role) + app.use(windowed) +} diff --git a/packages/im/client/components/list/chat-list.vue b/packages/im/client/components/list/chat-list.vue new file mode 100644 index 0000000..45f399c --- /dev/null +++ b/packages/im/client/components/list/chat-list.vue @@ -0,0 +1,254 @@ + + + diff --git a/packages/im/client/components/list/friend-item.vue b/packages/im/client/components/list/friend-item.vue new file mode 100644 index 0000000..c2e0d1e --- /dev/null +++ b/packages/im/client/components/list/friend-item.vue @@ -0,0 +1,6 @@ + diff --git a/packages/im/client/components/list/guild-item.vue b/packages/im/client/components/list/guild-item.vue new file mode 100644 index 0000000..9107677 --- /dev/null +++ b/packages/im/client/components/list/guild-item.vue @@ -0,0 +1,6 @@ + diff --git a/packages/im/client/components/list/index.ts b/packages/im/client/components/list/index.ts new file mode 100644 index 0000000..a1b150d --- /dev/null +++ b/packages/im/client/components/list/index.ts @@ -0,0 +1,8 @@ +import { App } from 'vue' +import ChatList from './chat-list.vue' +import MessageList from './message-list.vue' + +export default function (app: App) { + app.component('k-im-chat-list', ChatList) + app.component('k-im-message-list', MessageList) +} diff --git a/packages/im/client/components/list/message-item.vue b/packages/im/client/components/list/message-item.vue new file mode 100644 index 0000000..82e48c2 --- /dev/null +++ b/packages/im/client/components/list/message-item.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/packages/im/client/components/list/message-list.vue b/packages/im/client/components/list/message-list.vue new file mode 100644 index 0000000..34e0ed1 --- /dev/null +++ b/packages/im/client/components/list/message-list.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/im/client/components/list/session-item.vue b/packages/im/client/components/list/session-item.vue new file mode 100644 index 0000000..e69de29 diff --git a/packages/im/client/components/role/avatar.vue b/packages/im/client/components/role/avatar.vue new file mode 100644 index 0000000..8fe9ada --- /dev/null +++ b/packages/im/client/components/role/avatar.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/im/client/components/role/index.ts b/packages/im/client/components/role/index.ts new file mode 100644 index 0000000..f9ee8f6 --- /dev/null +++ b/packages/im/client/components/role/index.ts @@ -0,0 +1,10 @@ +import { App } from 'vue' +import Avatar from './avatar.vue' +import Login from './login-form.vue' +import Tag from './role-tag.vue' + +export default function (app: App) { + app.component('k-im-avatar', Avatar) + app.component('k-im-login', Login) + app.component('k-im-tag', Tag) +} diff --git a/packages/im/client/components/role/login-form.vue b/packages/im/client/components/role/login-form.vue new file mode 100644 index 0000000..f6e9472 --- /dev/null +++ b/packages/im/client/components/role/login-form.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/packages/im/client/components/role/role-tag.vue b/packages/im/client/components/role/role-tag.vue new file mode 100644 index 0000000..650edb1 --- /dev/null +++ b/packages/im/client/components/role/role-tag.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages/im/client/components/tab/index.ts b/packages/im/client/components/tab/index.ts new file mode 100644 index 0000000..a548377 --- /dev/null +++ b/packages/im/client/components/tab/index.ts @@ -0,0 +1,6 @@ +import { App } from 'vue' +import Tab from './tab.vue' + +export default function (app: App) { + app.component('k-im-tab', Tab) +} diff --git a/packages/im/client/components/tab/tab-item.vue b/packages/im/client/components/tab/tab-item.vue new file mode 100644 index 0000000..4126114 --- /dev/null +++ b/packages/im/client/components/tab/tab-item.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/packages/im/client/components/tab/tab.vue b/packages/im/client/components/tab/tab.vue new file mode 100644 index 0000000..4c65b88 --- /dev/null +++ b/packages/im/client/components/tab/tab.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/im/client/components/windowed/index.ts b/packages/im/client/components/windowed/index.ts new file mode 100644 index 0000000..fb7b2be --- /dev/null +++ b/packages/im/client/components/windowed/index.ts @@ -0,0 +1,6 @@ +import { App, defineComponent } from 'vue' +import MessageWindow from './message-window.vue' + +export default function (app: App) { + app.component('k-im-window-message', MessageWindow) +} diff --git a/packages/im/client/components/windowed/message-window.vue b/packages/im/client/components/windowed/message-window.vue new file mode 100644 index 0000000..214e56b --- /dev/null +++ b/packages/im/client/components/windowed/message-window.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/im/client/index.ts b/packages/im/client/index.ts index 190facb..5c4f59f 100644 --- a/packages/im/client/index.ts +++ b/packages/im/client/index.ts @@ -1,5 +1,37 @@ -import { Context } from '@cordisjs/client' -import {} from '../src' +import { reactive } from 'vue' +import { Context, Dict } from '@cordisjs/client' +import HTTP from '@cordisjs/plugin-http' +import Satori, { Bot, Universal } from '@satorijs/core' + +import 'virtual:uno.css' + +import install from './components' +import IMClient from './index.vue' + +declare module '@cordisjs/client' { + interface Context {} +} + +interface CachedLogin { + user: Bot + login: Universal.Login | null +} + +interface CachedData { + guilds: Universal.Guild[] + friends: Universal.User[] +} export default (ctx: Context) => { + let loginUser = reactive>({}) + ctx.plugin(HTTP) + ctx.plugin(Satori) + + ctx.app.use(install) + ctx.page({ + name: 'IM-test', + path: '/im', + icon: 'activity:chat', + component: IMClient, + }) } diff --git a/packages/im/client/index.vue b/packages/im/client/index.vue new file mode 100644 index 0000000..e8b7135 --- /dev/null +++ b/packages/im/client/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/im/client/shared/index.ts b/packages/im/client/shared/index.ts new file mode 100644 index 0000000..e96c733 --- /dev/null +++ b/packages/im/client/shared/index.ts @@ -0,0 +1,14 @@ +import { useStorage } from '@cordisjs/client' +import { ImTypes } from '@satorijs/plugin-im' + +interface SharedConfig { + shouldLogin: boolean + currentUser: ImTypes.User | null +} + +const shared = useStorage('im', 1, () => ({ + shouldLogin: true, + currentUser: null, +})) + +export default shared From 42a788f1406acf63b2388258f3b711a59d6881dc Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:42:16 +0800 Subject: [PATCH 09/50] chore: setup im pack (webui) --- packages/im/package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/im/package.json b/packages/im/package.json index 910f811..e4468ba 100644 --- a/packages/im/package.json +++ b/packages/im/package.json @@ -1,5 +1,5 @@ { - "name": "@satorijs/plugin-im", + "name": "@satorijs/plugin-im-webui", "description": "IM for Satori", "version": "0.0.0", "type": "module", @@ -31,7 +31,8 @@ "cordis": { "service": { "required": [ - "webui" + "webui", + "im" ] } }, @@ -41,10 +42,13 @@ }, "devDependencies": { "@cordisjs/client": "^0.1.7", + "@cordisjs/plugin-http": "^0.5.3", "@cordisjs/plugin-webui": "^0.1.7", - "@satorijs/core": "^4.1.1" + "@satorijs/core": "^4.1.1", + "@satorijs/plugin-server": "^2.6.5" }, "dependencies": { + "cordis": "^3.17.4", "cosmokit": "^1.6.2" } } From 56681288fb4304a44eff0d2629ad78bc97cdb770 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Fri, 19 Jul 2024 10:42:55 +0800 Subject: [PATCH 10/50] feat(webui): add injections --- packages/im/src/index.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/im/src/index.ts b/packages/im/src/index.ts index d9b80e4..73a805f 100644 --- a/packages/im/src/index.ts +++ b/packages/im/src/index.ts @@ -1,23 +1,31 @@ -import { Context, Schema } from '@satorijs/core' +import { Context, Dict, Schema } from '@satorijs/core' +import {} from '@satorijs/plugin-server' import {} from '@cordisjs/plugin-webui' +import {} from '@satorijs/plugin-im' + +// export interface Data { +// serverUrl: string +// invitations: Dict> +// } export const name = 'im' -export const inject = { - required: ['webui'], -} +export const inject = ['webui', 'satori', 'satori.server', 'im.data', 'im.login', 'im.event'] export interface Config {} export const Config: Schema = Schema.object({}) export function apply(ctx: Context) { - ctx.webui.addEntry({ - base: import.meta.url, - dev: '../client/index.ts', - prod: [ - '../dist/index.js', - '../dist/style.css', - ], - }) + const entry = ctx.webui.addEntry( + { + base: import.meta.url, + dev: '../client/index.ts', + prod: ['../dist/index.js', '../dist/style.css'], + }, + () => ({ + // serverUrl: ctx.satori.server.url + // invitations: ctx['im.invitation'].invitations + }) + ) } From d2b6662de1bed3fabc6cfc6d729169b9451dc7b0 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Thu, 8 Aug 2024 15:36:46 +0800 Subject: [PATCH 11/50] feat(webui): add icons --- packages/im/client/icons/ban.vue | 9 ++++ packages/im/client/icons/bell.vue | 9 ++++ packages/im/client/icons/channel/text.vue | 9 ++++ packages/im/client/icons/chat.vue | 9 ++++ packages/im/client/icons/circle-check.vue | 9 ++++ packages/im/client/icons/copy.vue | 9 ++++ packages/im/client/icons/edit.vue | 9 ++++ packages/im/client/icons/editor/bold.vue | 9 ++++ packages/im/client/icons/editor/italic.vue | 9 ++++ packages/im/client/icons/envelope.vue | 9 ++++ packages/im/client/icons/group.vue | 9 ++++ packages/im/client/icons/image.vue | 9 ++++ packages/im/client/icons/index.ts | 50 ++++++++++++++++++++++ packages/im/client/icons/minus.vue | 9 ++++ packages/im/client/icons/pin.vue | 9 ++++ packages/im/client/icons/quote.vue | 9 ++++ packages/im/client/icons/recall.vue | 9 ++++ packages/im/client/icons/refresh.vue | 9 ++++ packages/im/client/icons/setting.vue | 9 ++++ packages/im/client/icons/share.vue | 9 ++++ packages/im/client/icons/smile-wink.vue | 9 ++++ packages/im/client/icons/trash-can.vue | 9 ++++ 22 files changed, 239 insertions(+) create mode 100644 packages/im/client/icons/ban.vue create mode 100644 packages/im/client/icons/bell.vue create mode 100644 packages/im/client/icons/channel/text.vue create mode 100644 packages/im/client/icons/chat.vue create mode 100644 packages/im/client/icons/circle-check.vue create mode 100644 packages/im/client/icons/copy.vue create mode 100644 packages/im/client/icons/edit.vue create mode 100644 packages/im/client/icons/editor/bold.vue create mode 100644 packages/im/client/icons/editor/italic.vue create mode 100644 packages/im/client/icons/envelope.vue create mode 100644 packages/im/client/icons/group.vue create mode 100644 packages/im/client/icons/image.vue create mode 100644 packages/im/client/icons/index.ts create mode 100644 packages/im/client/icons/minus.vue create mode 100644 packages/im/client/icons/pin.vue create mode 100644 packages/im/client/icons/quote.vue create mode 100644 packages/im/client/icons/recall.vue create mode 100644 packages/im/client/icons/refresh.vue create mode 100644 packages/im/client/icons/setting.vue create mode 100644 packages/im/client/icons/share.vue create mode 100644 packages/im/client/icons/smile-wink.vue create mode 100644 packages/im/client/icons/trash-can.vue diff --git a/packages/im/client/icons/ban.vue b/packages/im/client/icons/ban.vue new file mode 100644 index 0000000..b5068ae --- /dev/null +++ b/packages/im/client/icons/ban.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/bell.vue b/packages/im/client/icons/bell.vue new file mode 100644 index 0000000..52b6f97 --- /dev/null +++ b/packages/im/client/icons/bell.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/channel/text.vue b/packages/im/client/icons/channel/text.vue new file mode 100644 index 0000000..33e8ad4 --- /dev/null +++ b/packages/im/client/icons/channel/text.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/chat.vue b/packages/im/client/icons/chat.vue new file mode 100644 index 0000000..52ed281 --- /dev/null +++ b/packages/im/client/icons/chat.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/circle-check.vue b/packages/im/client/icons/circle-check.vue new file mode 100644 index 0000000..fefcbb3 --- /dev/null +++ b/packages/im/client/icons/circle-check.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/copy.vue b/packages/im/client/icons/copy.vue new file mode 100644 index 0000000..87e5194 --- /dev/null +++ b/packages/im/client/icons/copy.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/edit.vue b/packages/im/client/icons/edit.vue new file mode 100644 index 0000000..ae6e957 --- /dev/null +++ b/packages/im/client/icons/edit.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/editor/bold.vue b/packages/im/client/icons/editor/bold.vue new file mode 100644 index 0000000..ebc1790 --- /dev/null +++ b/packages/im/client/icons/editor/bold.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/editor/italic.vue b/packages/im/client/icons/editor/italic.vue new file mode 100644 index 0000000..9898465 --- /dev/null +++ b/packages/im/client/icons/editor/italic.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/envelope.vue b/packages/im/client/icons/envelope.vue new file mode 100644 index 0000000..8dae5bb --- /dev/null +++ b/packages/im/client/icons/envelope.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/group.vue b/packages/im/client/icons/group.vue new file mode 100644 index 0000000..166da94 --- /dev/null +++ b/packages/im/client/icons/group.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/image.vue b/packages/im/client/icons/image.vue new file mode 100644 index 0000000..4a94bb5 --- /dev/null +++ b/packages/im/client/icons/image.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/index.ts b/packages/im/client/icons/index.ts new file mode 100644 index 0000000..9c2ed31 --- /dev/null +++ b/packages/im/client/icons/index.ts @@ -0,0 +1,50 @@ +import { icons, IconSquareCheck } from '@cordisjs/client' +import Ban from './ban.vue' +import Bell from './bell.vue' +import CircleCheck from './circle-check.vue' +import Copy from './copy.vue' +import Edit from './edit.vue' +import Envelope from './envelope.vue' +import Group from './group.vue' +import Image from './image.vue' +import Chat from './chat.vue' +import Minus from './minus.vue' +import SmileWink from './smile-wink.vue' +import Pin from './pin.vue' +import Quote from './quote.vue' +import Recall from './recall.vue' +import Refresh from './refresh.vue' +import Setting from './setting.vue' +import Share from './share.vue' +import TrashCan from './trash-can.vue' + +import TextChannel from './channel/text.vue' + +import Bold from './editor/bold.vue' +import Italic from './editor/italic.vue' + +icons.register('im:ban', Ban) +icons.register('im:bell', Bell) +icons.register('im:chat', Chat) +icons.register('im:checked', CircleCheck) +icons.register('im:copy', Copy) +icons.register('im:edit', Edit) +icons.register('im:envelope', Envelope) +icons.register('im:emoji', SmileWink) +icons.register('im:group', Group) +icons.register('im:image', Image) +icons.register('im:minus', Minus) +icons.register('im:pin', Pin) +icons.register('im:quote', Quote) +icons.register('im:readed', IconSquareCheck) +icons.register('im:recall', Recall) +icons.register('im:refresh', Refresh) +icons.register('im:setting', Setting) +icons.register('im:share', Share) +icons.register('im:trash-can', TrashCan) + +icons.register('im:text-channel', TextChannel) + +// editor +icons.register('editor:bold', Bold) +icons.register('editor:italic', Italic) diff --git a/packages/im/client/icons/minus.vue b/packages/im/client/icons/minus.vue new file mode 100644 index 0000000..cedfc6e --- /dev/null +++ b/packages/im/client/icons/minus.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/pin.vue b/packages/im/client/icons/pin.vue new file mode 100644 index 0000000..23110f7 --- /dev/null +++ b/packages/im/client/icons/pin.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/quote.vue b/packages/im/client/icons/quote.vue new file mode 100644 index 0000000..cb153c5 --- /dev/null +++ b/packages/im/client/icons/quote.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/recall.vue b/packages/im/client/icons/recall.vue new file mode 100644 index 0000000..8427256 --- /dev/null +++ b/packages/im/client/icons/recall.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/refresh.vue b/packages/im/client/icons/refresh.vue new file mode 100644 index 0000000..f0b7bef --- /dev/null +++ b/packages/im/client/icons/refresh.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/setting.vue b/packages/im/client/icons/setting.vue new file mode 100644 index 0000000..34ab0d4 --- /dev/null +++ b/packages/im/client/icons/setting.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/share.vue b/packages/im/client/icons/share.vue new file mode 100644 index 0000000..bfff7a9 --- /dev/null +++ b/packages/im/client/icons/share.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/smile-wink.vue b/packages/im/client/icons/smile-wink.vue new file mode 100644 index 0000000..55889e2 --- /dev/null +++ b/packages/im/client/icons/smile-wink.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/trash-can.vue b/packages/im/client/icons/trash-can.vue new file mode 100644 index 0000000..eee251b --- /dev/null +++ b/packages/im/client/icons/trash-can.vue @@ -0,0 +1,9 @@ + From 6217ab0405d0f252608f2697c4e8af5cf535dfe3 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Thu, 8 Aug 2024 15:37:28 +0800 Subject: [PATCH 12/50] chore: update dependencies --- packages/core/package.json | 109 ++++++++++++++++++------------------- packages/im/package.json | 15 +++-- 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 2a8476c..9bfa291 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,57 +1,56 @@ { - "name": "@satorijs/plugin-im", - "description": "Background for Satori IM", - "version": "0.0.0", - "type": "module", - "main": "lib/index.js", - "files": [ - "lib", - "dist" - ], - "author": "Shigma ", - "license": "MIT", - "scripts": { - "lint": "eslint src --ext .ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/koishijs/koishi-plugin-im.git", - "directory": "packages/im" - }, - "bugs": { - "url": "https://github.com/koishijs/koishi-plugin-im/issues" - }, - "keywords": [ - "satori", - "plugin", - "chat", - "im", - "database" - ], - "cordis": { - "service": { - "required": [ - "database-mysql" - ], - "implements": [ - "im.data" - ] - } - }, - "peerDependencies": { - "@satorijs/core": "^4.1.1" - }, - "devDependencies": { - "@cordisjs/plugin-webui": "^0.1.7", - "@satorijs/core": "^4.1.1", - "@types/uuid": "^10", - "uuid": "^10.0.0" - }, - "dependencies": { - "@minatojs/driver-mysql": "^3.4.1", - "cordis": "^3.17.4", - "cosmokit": "^1.6.2", - "minato": "^3.4.3" - } + "name": "@satorijs/plugin-im", + "description": "Background for Satori IM", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/im" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "chat", + "im", + "database" + ], + "cordis": { + "service": { + "required": [ + "database-mysql" + ], + "implements": [ + "im.data" + ] + } + }, + "peerDependencies": { + "@satorijs/core": "^4.1.1" + }, + "devDependencies": { + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.1.1", + "@types/uuid": "^10", + "uuid": "^10.0.0" + }, + "dependencies": { + "@minatojs/driver-mysql": "^3.4.1", + "cordis": "^3.17.4", + "cosmokit": "^1.6.2", + "minato": "^3.4.3" } - \ No newline at end of file +} diff --git a/packages/im/package.json b/packages/im/package.json index e4468ba..498b4b4 100644 --- a/packages/im/package.json +++ b/packages/im/package.json @@ -37,18 +37,21 @@ } }, "peerDependencies": { - "@cordisjs/plugin-webui": "^0.1.7", + "@cordisjs/plugin-webui": "^0.1.12", "@satorijs/core": "^4.1.1" }, "devDependencies": { - "@cordisjs/client": "^0.1.7", + "@cordisjs/client": "^0.1.12", "@cordisjs/plugin-http": "^0.5.3", - "@cordisjs/plugin-webui": "^0.1.7", + "@cordisjs/plugin-webui": "^0.1.12", "@satorijs/core": "^4.1.1", - "@satorijs/plugin-server": "^2.6.5" + "@satorijs/plugin-server": "^2.6.5", + "@types/markdown-it": "^14" }, "dependencies": { - "cordis": "^3.17.4", - "cosmokit": "^1.6.2" + "cordis": "^3.17.7", + "cosmokit": "^1.6.2", + "markdown-it": "^14.1.0", + "markdown-it-highlightjs": "^4.1.0" } } From 570df2e33445a6557dc8bba418a73dd644b03d58 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Thu, 8 Aug 2024 15:41:12 +0800 Subject: [PATCH 13/50] feat(webui): implement client unicast --- packages/im/src/entry.ts | 57 ++++++++++++++++++++++++++++++++++++++++ packages/im/src/index.ts | 37 ++++++++++++-------------- 2 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 packages/im/src/entry.ts diff --git a/packages/im/src/entry.ts b/packages/im/src/entry.ts new file mode 100644 index 0000000..f7b7dfe --- /dev/null +++ b/packages/im/src/entry.ts @@ -0,0 +1,57 @@ +import { Context, Service } from 'cordis' +import { Client, Entry } from '@cordisjs/plugin-webui' + +export class ImEntry extends Entry { + constructor( + public ctx: Context, + public files: Entry.Files, + public data: (client: Client) => any + ) { + super(ctx, files, data) + } + + unicast(clientId: string, data: any) { + const client = this.ctx.webui.clients[clientId] + if (client) { + const payload = { type: 'entry:update', body: { id: this.id, data } } + client.socket.send(JSON.stringify(payload)) + } + } + + unirefresh(clientId: string) { + const client = this.ctx.webui.clients[clientId] + if (client) { + const payload = { type: 'entry:refresh', body: { id: client.id, data: this.data?.(client) } } + client.socket.send(JSON.stringify(payload)) + } + } + + unipatch(clientId: string, data: any, key?: string) { + const client = this.ctx.webui.clients[clientId] + if (client) { + const payload = { type: 'entry:patch', body: { id: this.id, data, key } } + client.socket.send(JSON.stringify(payload)) + } + } +} + +export class EntryService extends Service { + static inject = ['webui', 'server'] + public entry: ImEntry + + constructor(ctx: Context) { + super(ctx, 'im.entry') + this.entry = new ImEntry( + ctx, + { + base: import.meta.url, + dev: '../client/index.ts', + prod: ['../dist/index.js', '../dist/style.css'], + }, + (client: Client) => ({ + serverUrl: ctx.server.selfUrl, + eventChan: {}, + }) + ) + } +} diff --git a/packages/im/src/index.ts b/packages/im/src/index.ts index 73a805f..fa87bb9 100644 --- a/packages/im/src/index.ts +++ b/packages/im/src/index.ts @@ -1,31 +1,28 @@ -import { Context, Dict, Schema } from '@satorijs/core' +import { Context, Schema, Service } from '@satorijs/core' import {} from '@satorijs/plugin-server' -import {} from '@cordisjs/plugin-webui' -import {} from '@satorijs/plugin-im' +import { Client } from '@cordisjs/plugin-webui' +import { ImTypes } from '@satorijs/plugin-im' +import { EntryService } from './entry' -// export interface Data { -// serverUrl: string -// invitations: Dict> -// } +export interface Data { + serverUrl: string + eventChan: ImTypes.Event +} export const name = 'im' -export const inject = ['webui', 'satori', 'satori.server', 'im.data', 'im.login', 'im.event'] +declare module 'cordis' { + interface Context { + 'im.entry': EntryService + } +} export interface Config {} +export const inject = ['im.event'] + export const Config: Schema = Schema.object({}) -export function apply(ctx: Context) { - const entry = ctx.webui.addEntry( - { - base: import.meta.url, - dev: '../client/index.ts', - prod: ['../dist/index.js', '../dist/style.css'], - }, - () => ({ - // serverUrl: ctx.satori.server.url - // invitations: ctx['im.invitation'].invitations - }) - ) +export default function apply(ctx: Context) { + ctx.plugin(EntryService) } From a3f83a21a40721f1d48aeec60e5c1ec585b2455b Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Thu, 8 Aug 2024 15:43:06 +0800 Subject: [PATCH 14/50] chore: move index.vue --- packages/im/client/components/index.vue | 61 +++++++++++++++++++++++++ packages/im/client/index.vue | 43 ----------------- 2 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 packages/im/client/components/index.vue delete mode 100644 packages/im/client/index.vue diff --git a/packages/im/client/components/index.vue b/packages/im/client/components/index.vue new file mode 100644 index 0000000..4dc70c6 --- /dev/null +++ b/packages/im/client/components/index.vue @@ -0,0 +1,61 @@ + + + diff --git a/packages/im/client/index.vue b/packages/im/client/index.vue deleted file mode 100644 index e8b7135..0000000 --- a/packages/im/client/index.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - From fcd3c0338eeabd0ec9b4fea960fce8d83dc7c58d Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 15:49:45 +0800 Subject: [PATCH 15/50] chore: remove uuid dependency --- packages/core/package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 9bfa291..97db1ef 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,27 +30,27 @@ ], "cordis": { "service": { - "required": [ - "database-mysql" - ], "implements": [ - "im.data" + "im.auth", + "im.data", + "im.event", + "im" ] } }, "peerDependencies": { - "@satorijs/core": "^4.1.1" + "@satorijs/core": "^4.2.6" }, "devDependencies": { - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.1.1", - "@types/uuid": "^10", - "uuid": "^10.0.0" + "@cordisjs/plugin-server": "^0.2.3", + "@satorijs/core": "^4.2.6", + "@types/mime-types": "^2" }, "dependencies": { - "@minatojs/driver-mysql": "^3.4.1", - "cordis": "^3.17.4", + "@minatojs/driver-mysql": "^3.5.0", + "cordis": "^3.18.0", "cosmokit": "^1.6.2", - "minato": "^3.4.3" + "mime-types": "^2.1.35", + "minato": "^3.5.1" } } From 4c9c285708b495962e4d1a308d2d3a5564634a65 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:02:50 +0800 Subject: [PATCH 16/50] refa: seperate the part of ctx.webui --- packages/core/src/data.ts | 4 +- packages/core/src/index.ts | 80 ++++++++++------------------- packages/core/src/models/channel.ts | 12 ++--- packages/core/src/models/friend.ts | 6 +-- packages/core/src/models/guild.ts | 5 +- packages/core/src/models/user.ts | 5 +- 6 files changed, 38 insertions(+), 74 deletions(-) diff --git a/packages/core/src/data.ts b/packages/core/src/data.ts index 646bbce..8bfc2c6 100644 --- a/packages/core/src/data.ts +++ b/packages/core/src/data.ts @@ -1,7 +1,7 @@ import { Context, Service } from '@satorijs/core' -export class ImDataService extends Service { - static inject = ['model', 'database', 'webui'] +class ImDataService extends Service { + static inject = ['server', 'model', 'database'] // FIXME: self injection constructor(public ctx: Context) { super(ctx, 'im.data', true) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 572b937..7f4ef49 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,63 +1,39 @@ -import {} from 'minato' -import {} from '@cordisjs/plugin-webui' -import { Context } from '@satorijs/core' - +import { Context, Service } from '@satorijs/core' import ImDatabase from './database' -import { ImDataService } from './data' -import { ImEventService } from './notify' -import { ImLoginService } from './login' -import { Channel, Friend, Guild, Member, Message, Role, User } from './types' - -export * as ImTypes from './types' -export * from './utils' - -declare module '@cordisjs/plugin-webui' { - interface Events { - 'im/v1/user/fetch'(uid: string): Promise - 'im/v1/user/update'(data: { uid: string } & Partial): Promise - 'im/v1/user/remove'(uid: string): Promise - - 'im/v1/friend/fetch'(uid: string, target: string): Promise - 'im/v1/friend/fetch-all'(uid: string): Promise> - 'im/v1/friend/remove'(uid: string, target: string): Promise - - 'im/v1/guild/fetch'(gid: string): Promise> - 'im/v1/guild/fetch-all'(uid: string): Promise> - 'im/v1/guild/create'(data: { name: string }): Promise - 'im/v1/guild/remove'(gid: string): Promise - - 'im/guild-member/fetch'( - uid: string, - gid: string - ): Promise & { role: Role }> - 'im/v1/guild-member/fetch-all'( - gid: string - ): Promise & { role: Role }>> - - 'im/v1/guild-role/fetch'(data: { gid: string; rid: string }): Promise - 'im/v1/guild-role/fetch-all'(gid: string): Promise> - 'im/v1/guild-role/create'(uid: string, gid: string): Promise - - 'im/v1/channel/fetch'(cid: string): Promise - 'im/v1/channel/create'(gid: string, data: { name: string }): Promise - 'im/v1/channel/remove'(cid: string): Promise +import { ImAuthService } from './auth' +import ImDataService from './data' +import { ImEventService } from './notifier' - 'im/v1/message/fetch'(id: string): Promise - 'im/v1/message/fetch-all'(data: { cid: string; uid?: string }): Promise> - } -} +export * as Im from './types' declare module 'cordis' { interface Context { + im: Im 'im.data': ImDataService 'im.event': ImEventService - 'im.login': ImLoginService + 'im.auth': ImAuthService } } -export function apply(ctx: Context) { - new ImDatabase(ctx) - ctx.plugin(ImDataService) - ctx.plugin(ImLoginService) - ctx.plugin(ImEventService) +declare module '@satorijs/core' { + interface Events { + 'exit'(signal: NodeJS.Signals): Promise + } +} + +interface Im { + data: ImDataService + event: ImEventService + auth: ImAuthService +} + +class Im extends Service { + constructor(public ctx: Context) { + super(ctx, 'im', true) + new ImDatabase(ctx) + ctx.plugin(ImDataService) + ctx.plugin(ImEventService) + ctx.plugin(ImAuthService) + } } +export default Im diff --git a/packages/core/src/models/channel.ts b/packages/core/src/models/channel.ts index aa8d338..2d9cc56 100644 --- a/packages/core/src/models/channel.ts +++ b/packages/core/src/models/channel.ts @@ -1,12 +1,10 @@ -import { Context, Universal } from '@satorijs/core' -import { Channel } from '../types' -import { genId } from '../utils' +import { $ } from 'minato' +import { Context } from '@satorijs/core' +import { Channel, Login } from '../types' +import { genId } from '@satorijs/plugin-im-utils' export class ChannelData { - constructor(public ctx: Context) { - ctx.webui.addListener('im/v1/channel/fetch', this.fetch) - ctx.webui.addListener('im/v1/channel/create', this.create) - } + constructor(public ctx: Context) {} async fetch(cid: string): Promise { const result = await this.ctx.database.get('satori-im.channel', { diff --git a/packages/core/src/models/friend.ts b/packages/core/src/models/friend.ts index e5741c7..930e568 100644 --- a/packages/core/src/models/friend.ts +++ b/packages/core/src/models/friend.ts @@ -3,11 +3,7 @@ import { Context } from '@satorijs/core' import { Friend } from '../types' export class FriendData { - constructor(public ctx: Context) { - ctx.webui.addListener('im/v1/friend/fetch', this.fetch) - ctx.webui.addListener('im/v1/friend/fetch-all', this.fetchAll) - ctx.webui.addListener('im/v1/friend/remove', this.hardDel) - } + constructor(public ctx: Context) {} async fetch(uid: string, target: string): Promise { const result = await this.ctx.database.get('satori-im.friend', (row) => diff --git a/packages/core/src/models/guild.ts b/packages/core/src/models/guild.ts index d93d338..ebc7f09 100644 --- a/packages/core/src/models/guild.ts +++ b/packages/core/src/models/guild.ts @@ -9,11 +9,8 @@ declare module '@cordisjs/plugin-webui' { export class GuildData { public cache?: Universal.List + constructor(public ctx: Context) {} - constructor(public ctx: Context) { - ctx.webui.addListener('im/v1/guild/fetch', this.fetch) - ctx.webui.addListener('im/v1/guild/fetch-all', this.list) - ctx.webui.addListener('im/v1/guild/create', this.create) ctx.webui.addListener('im/v1/guild-member/fetch-all', this.Member.list) } diff --git a/packages/core/src/models/user.ts b/packages/core/src/models/user.ts index 8194bfb..8a23ef0 100644 --- a/packages/core/src/models/user.ts +++ b/packages/core/src/models/user.ts @@ -2,10 +2,7 @@ import { Context } from '@satorijs/core' import { User } from '../types' export class UserData { - constructor(public ctx: Context) { - ctx.webui.addListener('im/v1/user/fetch', this.fetch) - ctx.webui.addListener('im/v1/user/update', this.update) - ctx.webui.addListener('im/v1/user/remove', this.softDel) + constructor(public ctx: Context) {} } async fetch(uid: string): Promise { From 85a3699131db86cf04ccad791f4ebd36f1e9f721 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:03:25 +0800 Subject: [PATCH 17/50] feat(core): initial support for assets --- packages/core/src/data.ts | 99 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/core/src/data.ts b/packages/core/src/data.ts index 8bfc2c6..faf89e3 100644 --- a/packages/core/src/data.ts +++ b/packages/core/src/data.ts @@ -1,9 +1,104 @@ -import { Context, Service } from '@satorijs/core' +import mime from 'mime-types' +import fs from 'fs/promises' +import path from 'path' +import { Context, Service, Schema } from '@satorijs/core' +import {} from '@cordisjs/plugin-server' +import * as Model from './models' +import { Login } from './types' +import { genId } from '@satorijs/plugin-im-utils' class ImDataService extends Service { static inject = ['server', 'model', 'database'] // FIXME: self injection - constructor(public ctx: Context) { + user: Model.UserData + friend: Model.FriendData + guild: Model.GuildData + channel: Model.ChannelData + message: Model.MessageData + notification: Model.NotificationData + + constructor(public ctx: Context, public config: ImDataService.Config) { super(ctx, 'im.data', true) + + // TODO: need better injections. + this.user = new Model.UserData(this.ctx) + this.friend = new Model.FriendData(this.ctx) + this.guild = new Model.GuildData(this.ctx) + this.channel = new Model.ChannelData(this.ctx) + this.message = new Model.MessageData(this.ctx) + this.notification = new Model.NotificationData(this.ctx) + + ctx.server.get('/im/v1/proxy/:url(.+)', async (koa) => { + const filePath = path.resolve(path.join(this.config.assetPath, koa.params.url)) + + try { + const fileContent = await fs.readFile(filePath) + koa.set( + 'Content-Type', + mime.contentType(path.extname(filePath)) || 'application/octet-stream' + ) + koa.body = fileContent + koa.status = 200 + } catch { + koa.status = 404 + } + }) + } + + writeAvatar = async (login: Login, b64: string, gid?: string) => { + let id = login.selfId + if (gid) { + if (!(await this.guild._isAuthorized(gid, login.selfId!))) { + throw new Error() + } + id = gid + } + + const mType = b64.match(/^data:(.*?);base64,/) + if (!mType) throw Error() + + const ext = mime.extension(mType[1]) + const filePath = path.join('avatars', `${gid ? 'g' : 'u'}-${login.selfId}.${ext}`) + const filePathAbsolute = path.join(this.config.assetPath, filePath) + + const fileData = b64.split('base64,')[1] + await fs.mkdir(path.dirname(filePathAbsolute), { recursive: true }) + await fs.writeFile(filePathAbsolute, Buffer.from(fileData, 'base64')) + + const urlPath = encodeURI(filePath.replace(/\\/g, '/')) + + gid + ? await this.guild._update(gid, { avatarUrl: urlPath }) + : await this.user._update(login.selfId!, { avatarUrl: urlPath }) + + return urlPath + } + + // TODO: + writeFile = async (login: Login, b64: string) => { + const mType = b64.match(/^data:(.*?);base64,/) + if (!mType) throw Error() + const ext = mime.extension(mType[1]) + const filePath = path.join(this.config.assetPath, 'messages') + + await fs.mkdir(filePath, { recursive: true }) + await fs.writeFile( + filePath + `/${genId()}-${login.selfId}.tmp.${ext}`, + Buffer.from(b64, 'base64') + ) + + return `${this.ctx.server.selfUrl}/${filePath}` } } + +namespace ImDataService { + export interface Config { + assetPath: string + } + + export const Config: Schema = Schema.object({ + assetPath: Schema.string().default('assets/im/'), + }) +} + +export default ImDataService From 61275b7710731e57835599d803ed8eba5dcedc93 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:09:20 +0800 Subject: [PATCH 18/50] chore: rename files --- packages/core/src/auth.ts | 134 ++++++++++++++++++++++++++++++++++ packages/core/src/login.ts | 58 --------------- packages/core/src/notifier.ts | 68 +++++++++++++++++ packages/core/src/notify.ts | 63 ---------------- 4 files changed, 202 insertions(+), 121 deletions(-) create mode 100644 packages/core/src/auth.ts delete mode 100644 packages/core/src/login.ts create mode 100644 packages/core/src/notifier.ts delete mode 100644 packages/core/src/notify.ts diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts new file mode 100644 index 0000000..1f2f761 --- /dev/null +++ b/packages/core/src/auth.ts @@ -0,0 +1,134 @@ +import { createHash } from 'crypto' +import { Context, Dict, Service, Universal } from '@satorijs/core' + +import { Login, User } from './types' +import { genId, validate } from '@satorijs/plugin-im-utils' + +export class ImAuthService extends Service { + static inject = ['model', 'database'] + public logins: Dict = {} + + constructor(public ctx: Context) { + super(ctx, 'im.auth', true) + ctx.on('ready', this._load) + ctx.on('exit', this._save) + ctx.on('dispose', this._save) + } + + authenticate = async (name: string, password: string, clientId): Promise => { + const result = await this.ctx.database + .select( + 'satori-im.user.auth', + { + user: { name: name }, + password: this._toHash(password), + }, + { + user: true, + } + ) + .execute() + if (result.length === 0) { + throw Error() + } + const user = result[0].user + const token = this._generateToken() + const login = { + user, + token, + clientId: clientId, + status: Universal.Status.ONLINE, + updateAt: new Date().getTime(), + expiredAt: new Date().getTime() + ImLoginService.options.tokenExpiration, + } as Login + this.logins[token] = login + this.ctx.logger('im').info(`new account signed in. name: ${login.user!.name}`) + return login + } + + authenticateWithToken = async (login: Login): Promise => { + const result = await this._verifyToken(login.token) + result.clientId = login.clientId + this.ctx.logger('im').info(`new account signed in with token. name: ${result.user!.name}`) + return result + } + + register = async (name: string, password: string): Promise => { + if ( + !this._validate('name', name) || + !this._validate('password', password) || + !(await this.isNameUnique(name)) + ) { + throw Error() + } + const id = genId() + const { password: _, ...result } = await this.ctx.database.create('satori-im.user.auth', { + user: { id, name: name }, + password: this._toHash(password), + }) + this.ctx.logger('im').info(`new account signed up. name: ${name}, id: ${id}`) + return name + } + + logout = async (login: Login): Promise => { + await this.ctx.database.remove('satori-im.login', { token: login.token }) + delete this.logins[login.token] + } + + isNameUnique = async (name: string): Promise => { + const result = await this.ctx.database.get('satori-im.user', { name }) + if (result.length) { + return false + } + return true + } + + async _verifyToken(token: string): Promise { + const result = this.logins[token] + if (!result || result.expiredAt! < new Date().getTime()) { + if (result) { + this.ctx.database.remove('satori-im.login', { token: token }).then((data) => { + if (!data.removed) { + } + }) + throw Error('token expired') + } + throw Error('no token matched') + } + return result + } + + private _validate = validate + + private _load = async (): Promise => { + const result = await this.ctx.database.select('satori-im.login').execute() + result.map((record) => (this.logins[record.token] = record)) + this.ctx.logger('im').info(`loaded authorized clients, ${result.length} record(s) in total. `) + } + + private _save = async (): Promise => { + const result = await this.ctx.database.upsert('satori-im.login', Object.values(this.logins)) + if (!result.inserted) { + throw new Error() + } + this.ctx.logger('im').info(`saved authorized clients.`) + } + + private _generateToken(length = 16): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return new Array(length) + .fill(0) + .map(() => characters[Math.floor(Math.random() * characters.length)]) + .join('') + } + + private _toHash(password: string): string { + return createHash('sha256').update(password).digest('hex') + } +} + +export namespace ImLoginService { + export const options = { + tokenExpiration: 7 * 24 * 60 * 60 * 1000, + } +} diff --git a/packages/core/src/login.ts b/packages/core/src/login.ts deleted file mode 100644 index f206e9e..0000000 --- a/packages/core/src/login.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Context, Dict, Service, Universal } from '@satorijs/core' -import { Login, User } from './types' -import { UserData } from './models' -import { genId, CharValidator } from './utils' - -declare module '@cordisjs/plugin-webui' { - interface Events { - 'im/v1/user/login-add'(data: { name: string; password: string }): Promise - 'im/v1/user/login-change'(data: { uid: string; status: number }): Promise - 'im/v1/user/login-remove'(uid: string): Promise - 'im/v1/user/register'(data: { name: string; password: string }): Promise - } -} - -// TODO: password crypt? -export class ImLoginService extends Service { - static inject = ['model', 'database', 'webui'] - public cache: Dict = {} - - constructor(public ctx: Context) { - super(ctx, 'im.login', true) - - ctx.webui.addListener('im/v1/user/login-add', this.accessibility) - ctx.webui.addListener('im/v1/user/login-change', this.sync) - ctx.webui.addListener('im/v1/user/login-remove', this.logout) - ctx.webui.addListener('im/v1/user/register', this.register) - // ctx.on('dispose', this._save()) - } - - validate = new CharValidator().validate - - accessibility = async (data: { name: string; password: string }): Promise => { - const result = await this.ctx.database.get('satori-im.user', data) - if (result.length === 0) { - throw Error() - } - this.cache[result[0].id] = { - user: result[0], - status: Universal.Status.ONLINE, - updateAt: new Date().getTime(), - features: [], - proxyUrls: [], - } - return result[0] - } - - register = async (data: { name: string; password: string }): Promise => { - const user = { id: genId(), ...data } - const { password: _, ...result } = await this.ctx.database.create('satori-im.user', user) - return result - } - - sync = async (data: { uid: string; status: number }) => {} - - async logout(uid: string): Promise { - delete this.cache[uid] - } -} diff --git a/packages/core/src/notifier.ts b/packages/core/src/notifier.ts new file mode 100644 index 0000000..6da7e24 --- /dev/null +++ b/packages/core/src/notifier.ts @@ -0,0 +1,68 @@ +import { Context, Dict, Service, Universal } from '@satorijs/core' +import { Channel, Event, Friend, Guild, Login, Message, Notification, User } from './types' +import { genId, v1Wrapper } from '@satorijs/plugin-im-utils' + +declare module '@satorijs/core' { + interface Events { + 'im/subscribe'(data: { login: Login }): Promise + } +} + +export class ImEventService extends Service { + static inject = ['database', 'im', 'im.auth'] + private currentEventId: number // ? + eventChans: Dict = {} + + constructor(public ctx: Context) { + super(ctx, 'im.event', true) + this.currentEventId = 1 + } + + pushEvent(event: Omit) { + const data: Event = { + id: this.currentEventId, + platform: 'satori-im', + timestamp: new Date().getTime(), + ...event, + } + this.currentEventId += 1 + this._pushEvent(data) + } + + private _pushEvent(data: Event) { + const receivers: Dict = {} + if (data.guild) { + for (let i = 0; i < data.guild.members!.length; i++) { + receivers[data.guild.members![i].user.id] = true + } + } else if (data.friend) { + receivers[data.friend.self.id] = true + receivers[data.friend.target.id] = true + } else if (data.user) { + receivers[data.user.id] = true + } + this.ctx.logger('im.debug').info(data) + this.ctx.logger('im.debug').info(receivers) + + const logins = this.ctx.im.auth.logins + const flag: Dict = {} + + // TODO: should delete the expired? + // TODO: fit event size. + for (const key in logins) { + const userId = logins[key].user!.id + const clientId = logins[key].clientId! + if (receivers[userId]) { + if (!flag[userId]) { + this.eventChans[userId] = data + flag[userId] = true + } + this.ctx.inject(['im.entry'], (ctx) => { + ctx['im.entry'].entry.unicast(clientId, { + eventChan: this.eventChans[userId], + }) + }) + } + } + } +} diff --git a/packages/core/src/notify.ts b/packages/core/src/notify.ts deleted file mode 100644 index a2bd410..0000000 --- a/packages/core/src/notify.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Context, Dict, Service } from '@satorijs/core' -import { FriendData, GuildData } from './models' -import { Event, Notification } from './types' - -declare module '@cordisjs/plugin-webui' { - interface Events { - 'im/v1/invitation/friend'(uid: string, target: string, content?: string): Promise - 'im/v1/invitation/member'( - uid: string, - gid: string, - target: string, - content?: string - ): Promise - 'im/v1/invitation/reply'(uid: string, originId: string, reply: boolean): Promise - } -} - -export class ImEventService extends Service { - static inject = ['model', 'database', 'webui'] - private _chans: { - notification: Dict> - event: Dict - } - - constructor(public ctx: Context) { - super(ctx, 'im.event', true) - this._chans = { notification: {}, event: {} } - - ctx.webui.addListener('im/v1/invitation/friend', this.request) - ctx.webui.addListener('im/v1/invitation/member', this.request) - ctx.webui.addListener('im/v1/invitation/reply', this.reply) - } - - request = async (uid: string, target: string, gid?: string, content?: string) => { - const invitation = { self: { id: uid }, user: { id: target }, guild: { id: gid } } - // this._notificationChan[uid].push(...this.notifications[uid]) - } - - reply = async (uid: string, origin: string, reply: boolean, gid?: string) => { - const invitations = this._chans.notification[origin] - if (!invitations) { - throw Error() - } - const index = invitations.findIndex( - (value) => (gid === undefined || value.guild.id === gid) && value.user.id === uid - ) - if (index === -1) { - throw Error() - } - if (reply === true) { - gid - ? await this.ctx.database.create('satori-im.member', { - user: { id: uid }, - guild: { id: gid }, - }) - : await this.ctx.database.create('satori-im.friend', { - origin: { id: origin }, - target: { id: uid }, - }) // TODO - } - invitations.splice(index, 1) - } -} From 46ac79cc98fcea51c36d4a6643292b13d66e7eea Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:09:47 +0800 Subject: [PATCH 19/50] feat(models): extend models --- packages/core/src/database.ts | 253 ++++++++++++++++++++++++---------- 1 file changed, 179 insertions(+), 74 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index af6d280..2cbd977 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -13,6 +13,20 @@ export default class ImDatabase { }, nick: 'char(255)', avatar: 'char(255)', + }, + { + primary: ['id'], + unique: ['name'], + } + ) + ctx.model.extend( + 'satori-im.user.auth', + { + user: { + type: 'oneToOne', + table: 'satori-im.user', + target: 'auth', + }, password: { type: 'char', length: 255, @@ -20,21 +34,20 @@ export default class ImDatabase { }, }, { - primary: ['id'], - unique: ['name'], + primary: ['user'], } ) ctx.model.extend( 'satori-im.user.settings', { - origin: { + user: { type: 'manyToOne', table: 'satori-im.user', - target: 'settings.user', }, target: { type: 'manyToOne', table: 'satori-im.user', + target: 'settings', }, level: { type: 'unsigned', @@ -42,19 +55,50 @@ export default class ImDatabase { initial: 2, }, }, - { primary: ['origin', 'target'] } + { primary: ['user', 'target'] } ) ctx.model.extend( - 'satori-im.friend', + 'satori-im.user.preferences', { - origin: { + user: { type: 'manyToOne', table: 'satori-im.user', + target: 'perferences', }, - target: { + }, + { + primary: ['user'], + } + ) + // FIXME: combined unique doesnt support relation. + ctx.model.extend( + 'satori-im.friend', + { + id: 'char(255)', + 'self.id': 'char(255)', + 'target.id': 'char(255)', + createdAt: 'unsigned(8)', + deleted: { + type: 'boolean', + initial: false, + }, + }, + { + primary: ['id'], + unique: [['self.id', 'target.id']], + } + ) + ctx.model.extend( + 'satori-im.friend.settings', + { + user: { type: 'manyToOne', table: 'satori-im.user', - target: 'friends', + }, + friend: { + type: 'manyToOne', + table: 'satori-im.friend', + target: 'settings', }, group: 'char(255)', pinned: { @@ -64,7 +108,7 @@ export default class ImDatabase { nick: 'char(255)', }, { - primary: ['origin', 'target'], + primary: ['user', 'friend'], } ) ctx.model.extend( @@ -72,11 +116,16 @@ export default class ImDatabase { { id: 'char(255)', name: { - type: 'string', + type: 'char', length: 255, nullable: false, }, avatar: 'char(255)', + createdAt: 'unsigned(8)', + deleted: { + type: 'boolean', + initial: false, + }, }, { primary: ['id'], @@ -87,12 +136,12 @@ export default class ImDatabase { { guild: { type: 'manyToOne', - table: 'satori-im.user', - target: 'settings.guild', + table: 'satori-im.guild', + target: 'settings', }, user: { type: 'manyToOne', - table: 'satori-im.guild', + table: 'satori-im.user', }, group: 'char(255)', pinned: 'boolean', @@ -112,76 +161,45 @@ export default class ImDatabase { user: { type: 'manyToOne', table: 'satori-im.user', + target: 'members', }, name: 'char(255)', // name of member identity + createdAt: 'unsigned(8)', }, { primary: ['guild', 'user'], } ) + // FIXME: combined unique doesnt support relation. ctx.model.extend( 'satori-im.role', { - id: 'char(255)', - user: { + id: 'string', + users: { type: 'manyToMany', table: 'satori-im.user', target: 'roles', }, - // guild: { - // type: 'manyToOne', - // table: 'satori-im.guild', - // target: 'roles', - // nullable: false, - // }, - 'guild.id': 'char(255)', + guild: { + type: 'manyToOne', + table: 'satori-im.guild', + target: 'roles', + }, + gid: 'char(255)', name: { - type: 'string', + type: 'char', length: 255, nullable: false, }, color: 'integer', - position: 'integer', permissions: 'bigint', - hoist: 'boolean', - mentionable: 'boolean', }, { primary: ['id'], - unique: [['guild.id', 'name']], + unique: [['gid', 'name']], } ) - ctx.model.extend( - 'satori-im.message.test', - { - id: 'char(255)', - 'user.id': 'char(255)', - 'channel.id': 'char(255)', - 'guild.id': 'char(255)', - 'quote.id': 'char(255)', - content: 'text', - createdAt: 'unsigned(8)', - updatedAt: 'unsigned(8)', - }, - { - primary: ['id'], - } - ) - ctx.model.extend( - 'satori-im.message.settings', - { - message: { - type: 'manyToOne', - table: 'satori-im.message.test', - }, - user: { - type: 'manyToOne', - table: 'satori-im.user', - target: 'settings.message', - }, - }, - { primary: ['message', 'user'] } - ) + ctx.model.extend( 'satori-im.channel', { @@ -189,6 +207,20 @@ export default class ImDatabase { name: 'char(255)', type: 'unsigned(1)', parentId: 'char(255)', + friend: { + type: 'oneToOne', + table: 'satori-im.friend', + target: 'channel', + }, // FIXME: doesnt work. + guild: { + type: 'manyToOne', + table: 'satori-im.guild', + target: 'channels', + }, + deleted: { + type: 'boolean', + initial: false, + }, }, { primary: ['id'], @@ -200,11 +232,11 @@ export default class ImDatabase { user: { type: 'manyToOne', table: 'satori-im.user', - target: 'settings.channel', }, channel: { type: 'manyToOne', table: 'satori-im.channel', + target: 'settings', }, pinned: { type: 'boolean', @@ -215,38 +247,111 @@ export default class ImDatabase { length: 1, initial: 0, }, - lastRead: 'char(255)', + lastRead: { + type: 'unsigned', + length: 8, + initial: new Date().getTime(), + }, }, { primary: ['channel', 'user'], } ) ctx.model.extend( - 'satori-im.login', + 'satori-im.message.test', { + id: 'char(255)', user: { type: 'manyToOne', table: 'satori-im.user', - target: 'logins', }, + channel: { + type: 'manyToOne', + table: 'satori-im.channel', + target: 'messages', + }, + 'quote.id': 'string', + content: 'text', + createdAt: 'unsigned(8)', + updatedAt: 'unsigned(8)', + deleted: { + type: 'boolean', + initial: false, + }, + }, + { + primary: ['id'], + } + ) + ctx.model.extend( + 'satori-im.message.settings', + { + user: { + type: 'manyToOne', + table: 'satori-im.user', + }, + message: { + type: 'manyToOne', + table: 'satori-im.message.test', + target: 'settings', + }, + }, + { primary: ['message', 'user'] } + ) + ctx.model.extend( + 'satori-im.login', + { + token: 'char(255)', + clientId: 'char(255)', + selfId: 'char(255)', status: 'unsigned(1)', updateAt: 'unsigned(8)', + expiredAt: 'unsigned(8)', }, { - primary: ['user'], + primary: ['token'], } ) - ctx.model.extend('satori-im.notification', { - self: { - type: 'manyToOne', - table: 'satori-im.user', + ctx.model.extend( + 'satori-im.notification', + { + id: 'char(255)', + selfId: 'char(255)', + user: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'notifications', + }, + guild: { + type: 'manyToOne', + table: 'satori-im.guild', + }, + shouldReply: 'boolean', + content: 'char(255)', + createdAt: 'unsigned(8)', }, - user: { - type: 'manyToOne', - table: 'satori-im.user', + { + primary: ['id'], + autoInc: true, + } + ) + ctx.model.extend( + 'satori-im.notification.settings', + { + self: { + type: 'manyToOne', + table: 'satori-im.notification', + target: 'settings', + }, + user: { + type: 'manyToOne', + table: 'satori-im.user', + }, + read: 'boolean', }, - 'guild.id': 'char(255)', - type: 'unsigned(0)', - }) + { + primary: ['self', 'user'], + } + ) } } From 77a9211d98ff84b185f9a334bf0a3dce9b096a79 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:11:39 +0800 Subject: [PATCH 20/50] feat(core): add search and other APIs --- packages/core/src/models/channel.ts | 141 ++++++- packages/core/src/models/friend.ts | 234 +++++++++-- packages/core/src/models/guild.ts | 470 +++++++++++++++++++++-- packages/core/src/models/index.ts | 2 + packages/core/src/models/message.ts | 150 ++++++++ packages/core/src/models/notification.ts | 147 +++++++ packages/core/src/models/user.ts | 72 +++- 7 files changed, 1119 insertions(+), 97 deletions(-) create mode 100644 packages/core/src/models/notification.ts diff --git a/packages/core/src/models/channel.ts b/packages/core/src/models/channel.ts index 2d9cc56..f986fb4 100644 --- a/packages/core/src/models/channel.ts +++ b/packages/core/src/models/channel.ts @@ -6,29 +6,144 @@ import { genId } from '@satorijs/plugin-im-utils' export class ChannelData { constructor(public ctx: Context) {} - async fetch(cid: string): Promise { - const result = await this.ctx.database.get('satori-im.channel', { - id: cid, - }) + fetch = async (login: Login, cid: string): Promise => { + const result = await this.ctx.database + .select( + 'satori-im.channel', + { + id: cid, + deleted: false, + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, + }, + { + friend: true, + guild: true, + settings: true, + } + ) + .execute() return result[0] } - async create(gid: string, data: { name: string }): Promise { - return await this.ctx.database.create('satori-im.channel', { + fetchSessionList = async (login: Login): Promise> => { + const result = await this.ctx.database + .select( + 'satori-im.channel', + { + deleted: false, + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, + messages: { + $some: (row) => $.gt(row.createdAt, MessageData.recentEndpoint()), + }, + }, + { + settings: true, + guild: true, + friend: true, + messages: true, + } + ) + .project({ + id: (row) => row.id, + type: (row) => row.type, + name: (row) => row.name, + friend: (row) => row.friend, + guild: (row) => row.guild, + settings: (row) => row.settings, + }) + .execute() + return result + } + + create = async (login: Login, gid: string, name: string): Promise => { + const result = await this.ctx.database.create('satori-im.channel', { id: genId(), - name: data.name, - type: Universal.Channel.Type.TEXT, + name: name, + type: Channel.Type.TEXT, parentId: gid, }) + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'channel-added', + channel: result, + }) + return result + } + + update = async (login: Login, cid: string, name?: string) => { + const result = await this.ctx.database.set( + 'satori-im.channel', + { + id: cid, + deleted: false, + }, + { + name: name, + } + ) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'channel-updated', + channel: { id: cid, name } as Channel, + }) + } + + updateSettings = async (login: Login, cid: string) => { + await this.ctx.database.upsert('satori-im.channel.settings', (row) => [ + { + lastRead: new Date().getTime(), + channel: { id: cid }, + user: { id: login.selfId }, + }, + ]) } - async _update(cid: string, data: { name: string }): Promise { - const result = await this.ctx.database.set('satori-im.channel', cid, { - name: data.name, + softDel = async (login: Login, cid: string) => { + this.ctx.database.transact(async (db) => { + await db.set('satori-im.channel', cid, { + deleted: false, + }) + await db.remove('satori-im.channel.settings', { + channel: { id: cid }, + }) + }) + + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'channel-deleted', + channel: { id: cid } as Channel, }) } +} - async _softDel(cid: string): Promise { - const result = await this.ctx.database.remove('satori-im.channel', cid) +namespace MessageData { + export function recentEndpoint(): number { + const date = new Date() + date.setMonth(date.getMonth() - 3) + return date.getTime() } } diff --git a/packages/core/src/models/friend.ts b/packages/core/src/models/friend.ts index 930e568..956652f 100644 --- a/packages/core/src/models/friend.ts +++ b/packages/core/src/models/friend.ts @@ -1,49 +1,221 @@ import { $ } from 'minato' import { Context } from '@satorijs/core' -import { Friend } from '../types' +import { Channel, Friend, Login, ChunkOptions } from '../types' +import { genId } from '@satorijs/plugin-im-utils' export class FriendData { constructor(public ctx: Context) {} - async fetch(uid: string, target: string): Promise { - const result = await this.ctx.database.get('satori-im.friend', (row) => - $.or( - $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), - $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) + fetch = async (login: Login, fid: string): Promise => { + const selfId = login.selfId! + const result = await this.ctx.database + .join( + { + f: 'satori-im.friend', + u: 'satori-im.user', + us: 'satori-im.user.settings', + fs: 'satori-im.friend.settings', + c: 'satori-im.channel', + }, + (row) => + $.and( + $.or( + $.and($.eq(row.f.target.id, login.selfId!), $.eq(row.f.self.id, row.u.id)), + $.and($.eq(row.f.self.id, login.selfId!), $.eq(row.f.target.id, row.u.id)) + ), + $.eq(row.f.id, row.fs.friend.id), + $.eq(row.u.id, row.us.user.id), + $.eq(row.c.parentId, row.f.id), + $.eq(row.c.type, Channel.Type.DIRECT) + ), + { + f: false, + u: false, + c: false, + us: true, + fs: true, + } ) - ) + .where((row) => $.and($.ne(row.u.id, selfId), $.eq(row.f.id, fid))) + .project({ + id: (row) => row.f.id, + createdAt: (row) => row.f.createdAt, + user: (row) => row.u, + channel: (row) => row.c, + nick: (row) => row.fs.nick, + level: (row) => row.us.level, + pinned: (row) => row.fs.pinned, + group: (row) => row.fs.group, + }) + .execute() + + return result[0] + } + + _fetch = async (fid: string): Promise => { + const result = await this.ctx.database.get('satori-im.friend', { + id: fid, + }) return result[0] } - async fetchAll(uid: string): Promise> { - const result = await this.ctx.database.get('satori-im.friend', (row) => - $.or($.eq(row.origin.id, uid), $.eq(row.target.id, uid)) - ) + list = async (login: Login): Promise> => { + const selfId = login.selfId! + // const result = await this.ctx.database + // .select( + // 'satori-im.friend', + // (row) => $.or($.eq(row.target.id, selfId), $.eq(row.self.id, selfId)), + // { + // settings: true, + // channel: true, + // } + // ) + // .execute() + // result.map((value) => value.settings!.filter((value) => value.user.id === login.selfId)) + // return result + const result = await this.ctx.database + .join( + { + f: 'satori-im.friend', + u: 'satori-im.user', + c: 'satori-im.channel', + us: 'satori-im.user.settings', + fs: 'satori-im.friend.settings', + }, + (row) => $.and($.eq(row.f.id, row.fs.friend.id), $.eq(row.u.id, row.us.user.id)), + { + f: false, + u: false, + c: false, + us: true, + fs: true, + } + ) + .where((row) => + $.and( + $.or( + $.and($.eq(row.f.target.id, selfId), $.eq(row.f.self.id, row.u.id)), + $.and($.eq(row.f.self.id, selfId), $.eq(row.f.target.id, row.u.id)) + ), + $.ne(row.u.id, selfId), + $.eq(row.c.parentId, row.f.id), + $.eq(row.c.type, Channel.Type.DIRECT) + ) + ) + .project({ + id: (row) => row.f.id, + createdAt: (row) => row.f.createdAt, + user: (row) => row.u, + channel: (row) => row.c, + nick: (row) => row.fs.nick, + level: (row) => row.us.level, + pinned: (row) => row.fs.pinned, + group: (row) => row.fs.group, + }) + .execute() + return result } - async _update(uid: string, target: string, data: Partial): Promise { - const result = await this.ctx.database.set( - 'satori-im.friend', - (row) => - $.or( - $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), - $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) - ), - { - pinned: data.pinned, - nick: data.nick, - group: data.group, - } - ) + update = async (login: Login, fid: string, common?: {}, critical?: {}) => { + // const selfId = login.selfId! + // const result = await this.ctx.database.set( + // 'satori-im.friend', + // (row) => + // $.and($.or($.eq(row.target.id, selfId), $.eq(row.self.id, selfId)), $.eq(row.id, fid)), + // { + // pinned, + // nick: nick, + // group: group, + // } + // ) + // if (!result.matched) { + // throw new Error() + // } + // if (!result.modified) { + // throw new Error() + // } + // this.ctx.im.event.pushEvent({ + // type: 'friend-updated', + // selfId, + // friend: { id: fid, pinned, nick, group } as Friend, + // }) + } + + softDel = async (login: Login, fid: string) => { + const selfId = login.selfId! + this.ctx.database.transact(async (db) => { + await db.set( + 'satori-im.friend', + (row) => + $.and($.or($.eq(row.target.id, selfId), $.eq(row.self.id, selfId)), $.eq(row.id, fid)), + { + deleted: true, + } + ) + await db.set( + 'satori-im.channel', + { + parentId: fid, + type: Channel.Type.DIRECT, + }, + { + deleted: true, + } + ) + await db.remove('satori-im.friend.settings', { + friend: { id: fid }, + }) + await db.remove('satori-im.channel.settings', { + channel: { parentId: fid, type: Channel.Type.DIRECT }, + }) + }) + this.ctx.im.event.pushEvent({ + type: 'friend-deleted', + selfId, + friend: { id: fid } as Friend, + }) } - async hardDel(uid: string, target: string): Promise { - const result = await this.ctx.database.remove('satori-im.friend', (row) => - $.or( - $.and($.eq(row.origin.id, uid), $.eq(row.target.id, target)), - $.and($.eq(row.origin.id, target), $.eq(row.target.id, uid)) + search = async ( + login: Login, + keyword: string, + options: ChunkOptions & { except: Array } + ): Promise> => { + const keywordRegex = new RegExp(`${keyword}`, 'i') + let query = this.ctx.database + .join( + { f: 'satori-im.friend', user: 'satori-im.user', settings: 'satori-im.friend.settings' }, + (row) => + $.and( + $.or( + $.and($.eq(row.f.target.id, login.selfId!), $.eq(row.f.self.id, row.user.id)), + $.and($.eq(row.f.self.id, login.selfId!), $.eq(row.f.target.id, row.user.id)) + ), + $.or($.eq(row.f.id, row.settings.friend.id), $.eq(row.settings.friend.id, '')), + $.or( + $.regex(row.user.name, keywordRegex), + $.regex(row.user.nick, keywordRegex), + $.eq(row.user.id, keyword) + ), + ...options.except.map((value) => $.ne(row.user.id, value)) + ), + { + f: false, + user: false, + settings: true, + } ) - ) + .where((row) => $.ne(row.user.id, login.selfId!)) + if (options.cursor === '#begin') { + query = query.orderBy('user.id', 'asc') + } else if (options.direction === 'before') { + query = query.where((row) => $.lt(row.user.id, options.cursor)).orderBy('user.id', 'desc') + } else if (options.direction === 'after') { + query = query.where((row) => $.gt(row.user.id, options.cursor)).orderBy('user.id', 'asc') + } else { + throw Error() + } + return query.limit(options.limit).execute() } } diff --git a/packages/core/src/models/guild.ts b/packages/core/src/models/guild.ts index ebc7f09..ab32ff1 100644 --- a/packages/core/src/models/guild.ts +++ b/packages/core/src/models/guild.ts @@ -1,72 +1,466 @@ import { $ } from 'minato' -import { Context, Universal } from '@satorijs/core' -import { Guild, Member, Role } from '../types' -import { genId } from '../utils' - -declare module '@cordisjs/plugin-webui' { - interface Events {} -} +import { Context } from '@satorijs/core' +import { Channel, Guild, Login, Member, Role, ChunkOptions, extractSettings } from '../types' +import { genId } from '@satorijs/plugin-im-utils' export class GuildData { - public cache?: Universal.List constructor(public ctx: Context) {} + fetch = async (login: Login, gid: string): Promise => { + const result = await this.ctx.database + .select( + 'satori-im.guild', + { + id: gid, + members: { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + channels: { + $some: { + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, + }, + }, + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, + }, + { + channels: true, + settings: true, + members: true, + } + ) + .execute() - ctx.webui.addListener('im/v1/guild-member/fetch-all', this.Member.list) + return result[0] } - async fetch(gid: string): Promise> { - return this.ctx.database.get('satori-im.guild', gid, ['id', 'name', 'avatar']) + _fetch = async (gid: string): Promise => { + const result = await this.ctx.database + .select( + 'satori-im.guild', + { + id: gid, + }, + { + members: true, + settings: true, + channels: true, + } + ) + .execute() + return result[0] } - async list(uid: string): Promise> { - const result = await this.ctx.database.get('satori-im.member', { user: { id: uid } }, ['guild']) - return result.map((data) => { - return data.guild + list = async (login: Login): Promise> => { + const result = await this.ctx.database.get('satori-im.guild', { + members: { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + channels: { + $some: { + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, + }, + }, + settings: { + $or: [ + { + $some: (row) => $.eq(row.user.id, login.selfId!), + }, + { + $none: {}, + }, + ], + }, }) + return result as any } - async create(data: { name: string }): Promise { - const result = await this.ctx.database.create('satori-im.guild', { - id: genId(), - name: data.name, + create = async ( + login: Login, + name: string, + options: { inviteUsers?: string[]; initialChannelName?: string } + ): Promise => { + const selfId = login.selfId! + const timestamp = new Date().getTime() + const result: Guild = await this.ctx.database.transact(async (db): Promise => { + const result = await db.create('satori-im.guild', { + id: genId(), + name, + createdAt: timestamp, + }) + const owner = await db.create('satori-im.member', { + guild: result, + user: { $connect: { id: selfId } }, + }) + const role = await db.create('satori-im.role', { + id: genId(), + name: '$a', + guild: { $connect: { id: result.id } }, + gid: result.id, + users: { $connect: { id: selfId } }, + }) + const channel = await db.create('satori-im.channel', { + id: genId(), + parentId: result.id, + guild: { $connect: { id: result.id } }, + name: options.initialChannelName ? options.initialChannelName : '主要频道', + }) + + return { + ...result, + members: [ + { + ...owner, + roles: [role as any], + }, + ], + channels: [channel], + } + }) + + if (options.inviteUsers) { + const toInvite = options.inviteUsers + for (let i = 0; i < toInvite.length; i++) { + const friend = await this.ctx.database.get('satori-im.friend', (row) => + $.or( + $.and($.eq(row.self.id, login.selfId!), $.eq(row.target.id, toInvite[i])), + $.and($.eq(row.target.id, login.selfId!), $.eq(row.self.id, toInvite[i])) + ) + ) + if (friend) { + this.ctx.im.data.notification.request(login, toInvite[i], '', result.id) + } + } + } + + this.ctx['im.event'].pushEvent({ + selfId: login.selfId!, + type: 'guild-added', + guild: result, }) return result } - // TODO: permission check. - async _update(gid: string, data: Partial): Promise { - await this.ctx.database.set('satori-im.guild', gid, { - name: data.name, + update = async (login: Login, gid: string, name: string) => { + const result = await this.ctx.database.set('satori-im.guild', gid, { + name, + }) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + this.ctx['im.event'].pushEvent({ + type: 'guild-updated', + selfId: login.selfId!, + guild: { id: gid, name } as Guild, }) } - async _softDel(gid: string): Promise { - await this.ctx.database.remove('satori-im.guild', gid) + _update = async (gid: string, data: { name?: string; avatarUrl?: string }) => { + const result = await this.ctx.database.set( + 'satori-im.guild', + { id: gid }, + { + name: data.name, + avatar: data.avatarUrl, + } + ) + if (!result.matched) { + throw new Error() + } + const guild = await this._fetch(gid) + this.ctx['im.event'].pushEvent({ + type: 'guild-updated', + selfId: '', + guild, + }) } - public Member = { - fetch: async (uid: string, gid: string): Promise => { - const result = await this.ctx.database.get('satori-im.member', (row) => - $.and($.eq(row.user.id, uid), $.eq(row.guild.id, gid)) + softDel = async (login: Login, gid: string) => { + this.ctx.database.transact(async (db) => { + await db.set('satori-im.guild', gid, { deleted: true }) + await db.remove('satori-im.guild.settings', { + guild: { id: gid }, + }) + await db.set( + 'satori-im.channel', + { + parentId: gid, + type: { $not: Channel.Type.DIRECT }, + }, + { + deleted: true, + } ) - return result[0] + await db.remove('satori-im.channel.settings', { + channel: { parentId: gid, type: { $not: Channel.Type.DIRECT } }, + }) + }) + this.ctx['im.event'].pushEvent({ + type: 'guild-deleted', + selfId: login.selfId!, + guild: { id: gid } as Guild, + }) + } + + // HACK: shouldnt get guilds which the user has joined. + search = async ( + login: Login, + keyword: string, + options: ChunkOptions & { except: Array } + ): Promise> => { + const keywordRegex = new RegExp(`${keyword}`, 'i') + let query = this.ctx.database + .select('satori-im.guild') + .where((row) => + $.and( + $.or($.eq(row.id, keyword), $.regex(row.name, keywordRegex)), + ...options.except.map((value) => $.ne(row.id, value)) + ) + ) + if (options.cursor === '#begin') { + query = query.orderBy('id', 'asc') + } else if (options.direction === 'before') { + query = query.where((row) => $.lt(row.id, options.cursor)).orderBy('id', 'desc') + } else if (options.direction === 'after') { + query = query.where((row) => $.gt(row.id, options.cursor)).orderBy('id', 'asc') + } else { + throw Error() + } + if (options.offset) { + query = query.offset(options.offset) + } + return query.limit(options.limit).execute() + } + + async _isGuildMember(gid: string, uid: string): Promise { + const result = await this.ctx.database.get('satori-im.member', { + guild: { id: gid }, + user: { id: uid }, + }) + return result.length ? true : false + } + + async _isAuthorized(gid: string, uid: string): Promise { + // TODO: + return true + } + + public Member = { + fetch: async (login: Login, gid: string, uid: string): Promise => { + if (!(await this._isGuildMember(gid, login.selfId!))) + return { e: 'unauthorized' } as any as Member + + const result = await this.ctx.database + .select( + 'satori-im.member', + { + user: { + id: uid, + roles: { + $or: [ + { + $some: (row) => $.eq(row.guild.id, gid), + }, + { + $none: {}, + }, + ], + }, + }, + guild: { id: gid }, + }, + { + user: { + roles: true, + }, + } + ) + .project({ + name: (row) => row.name, + roles: (row) => row.user.roles, + guild: (row) => row.guild, + user: (row) => row.user, + }) + .execute() + + return result[0] as any }, - list: async (gid: string): Promise & { role: Role }>> => { + list: async (login: Login, gid: string): Promise> => { + if (!(await this._isGuildMember(gid, login.selfId!))) + return { e: 'unauthorized' } as any as Array + const result = await this.ctx.database - .join(['satori-im.member', 'satori-im.role'], (member, role) => - $.eq(member['user.id'], role.user) + .select( + 'satori-im.member', + { + user: { + roles: { + $or: [ + { + $some: { + guild: { id: gid }, + }, + }, + { + $none: { + guild: { id: gid }, + }, + }, + ], + }, + }, + guild: { id: gid }, + }, + { + user: { + roles: true, + }, + } ) - .where((row) => $.eq('guild.id', gid)) - .orderBy('satori-im.member.name', 'asc') .project({ - user: (row) => row['satori-im.member'].user, - role: (row) => row['satori-im.role'], - name: (row) => row['satori-im.member'].name, + name: (row) => row.name, + roles: (row) => row.user.roles, + guild: (row) => row.guild, + user: (row) => row.user, }) .execute() + return result as any + }, + + // TODO: Update member.role. + update: async (login: Login, gid: string, uid?: string, name?: string) => { + const result = await this.ctx.database.set( + 'satori-im.member', + { + guild: { id: gid }, + user: { id: login.selfId }, + }, + { + name: name, + } + ) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + this.ctx['im.event'].pushEvent({ + type: 'guild-member-updated', + selfId: login.selfId!, + member: { + guild: { id: gid }, + user: { id: login.selfId! }, + name: name, + }, + }) + }, + + kick: async (login: Login, gid: string, uid: string) => { + const result = await this.ctx.database.remove('satori-im.member', { + guild: { id: gid }, + user: { id: uid }, + }) + if (!result.matched) { + throw new Error() + } + if (!result.removed) { + throw new Error() + } + this.ctx['im.event'].pushEvent({ + type: 'guild-member-deleted', + selfId: login.selfId!, + member: { + guild: { id: gid }, + user: { id: login.selfId! }, + }, + }) + }, + } + + public Role = { + fetch: async (login: Login, gid: string, rid: string): Promise => { + const result = await this.ctx.database.get('satori-im.role', rid) + return result[0] + }, + + list: async (login: Login, gid: string): Promise> => { + const result = await this.ctx.database.get('satori-im.role', { + guild: { id: gid }, + }) return result }, + + create: async ( + login: Login, + gid: string, + uid: string, + name: string, + color: number + ): Promise => { + const result = await this.ctx.database.create('satori-im.role', { + id: genId(), + guild: { id: gid }, + gid, + users: [{ id: uid }], + name, + color, + }) + return result + }, + + // TODO: Add authority. + update: async (login: Login, rid: string, name?: string, color?: number) => { + const result = await this.ctx.database.set('satori-im.role', rid, { + name, + color, + }) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + }, + + hardDel: async (login: Login, rid: string) => { + const result = await this.ctx.database.remove('satori-im.role', rid) + if (!result.matched) { + throw new Error() + } + if (!result.removed) { + throw new Error() + } + }, } } diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index d79059e..e621847 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -2,3 +2,5 @@ export * from './channel' export * from './friend' export * from './user' export * from './guild' +export * from './message' +export * from './notification' diff --git a/packages/core/src/models/message.ts b/packages/core/src/models/message.ts index e69de29..6b69c38 100644 --- a/packages/core/src/models/message.ts +++ b/packages/core/src/models/message.ts @@ -0,0 +1,150 @@ +import { $ } from 'minato' +import { Context } from '@satorijs/core' +import { Channel, Friend, Login, Message, Guild, Member } from '../types' +import { genId } from '@satorijs/plugin-im-utils' + +export class MessageData { + constructor(public ctx: Context) {} + + fetch = async ( + login: Login, + cid: string, + endpoint: number, + dir: Message.Direction = 'before', + limit: number = 50 + ): Promise => { + let query = this.ctx.database + .select('satori-im.message.test', (row) => + $.and( + dir === 'before' ? $.lt(row.createdAt, endpoint) : $.gt(row.createdAt, endpoint), + $.not(row.deleted), + $.eq(row.channel.id, cid) + ) + ) + .orderBy('createdAt', dir === 'before' ? 'desc' : 'asc') + + if (limit > 0) { + query = query.limit(limit) + } + return query.execute() + } + + getUnreadCount = async (login: Login, cid: string): Promise => { + // TODO: + const channel = ( + await this.ctx.database.get('satori-im.channel', { + id: cid, + }) + )[0] + + const defaultLastRead = + channel?.type !== Channel.Type.DIRECT + ? ( + await this.ctx.database.get('satori-im.member', { + user: { id: login.selfId }, + guild: { id: channel.guild!.id }, + }) + )[0].createdAt! + : 0 + + let query = await this.ctx.database + .join( + { m: 'satori-im.message.test', cs: 'satori-im.channel.settings' }, + (row) => + $.and($.eq(row.m.channel.id, row.cs.channel.id), $.eq(row.cs.user.id, login.selfId!)), + { + m: false, + cs: true, + } + ) + .where((row) => + $.and( + $.eq(row.m.channel.id, cid), + $.or( + $.gt(row.m.createdAt, row.cs.lastRead), + $.and($.eq($.ifNull(row.cs.lastRead, -1), -1), $.gt(row.m.createdAt, defaultLastRead)) + ) + ) + ) + .groupBy('cs.user.id', { + unread: (row) => $.count(row.m), + }) + .execute() + + return query[0]?.unread || 0 + } + + create = async (login: Login, data: Message) => { + let guild: Guild | undefined = undefined + let member: Member | undefined = undefined + let friend: Friend | undefined = undefined + const channel = await this.ctx.im.data.channel.fetch(login, data.channel.id) + + if (data.channel!.type !== Channel.Type.DIRECT) { + guild = await this.ctx.im.data.guild.fetch(login, channel.guild!.id) + member = await this.ctx.im.data.guild.Member.fetch(login, guild.id, login.selfId!) + } else { + friend = await this.ctx.im.data.friend._fetch(channel.friend!.id) + } + + const result = await this.ctx.database.create('satori-im.message.test', { + id: genId(), + createdAt: new Date().getTime(), + content: data.content, + channel: { $literal: { id: data.channel!.id } }, + user: { $literal: { id: data.user!.id } }, + quote: { + id: data.quote?.id, + }, + }) + + const user = await this.ctx.im.data.user.fetch(login, login.selfId!) + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'message', + message: { ...result, user, member, sid: data.sid }, + channel: result.channel, + guild, + friend, + }) + } + + update = async (login: Login, cid: string, mid: string, content: string) => { + const result = await this.ctx.database.set( + 'satori-im.message.test', + { + id: mid, + deleted: false, + }, + { content } + ) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'message-updated', + message: { id: mid, content } as Message, + channel: { id: cid } as Channel, + }) + } + + softDel = async (login: Login, cid: string, mid: string) => { + const result = await this.ctx.database.set('satori-im.message.test', mid, { deleted: true }) + if (!result.matched) { + throw new Error() + } + if (!result.modified) { + throw new Error() + } + this.ctx.im.event.pushEvent({ + selfId: login.selfId!, + type: 'message-deleted', + message: { id: mid } as Message, + channel: { id: cid } as Channel, + }) + } +} diff --git a/packages/core/src/models/notification.ts b/packages/core/src/models/notification.ts new file mode 100644 index 0000000..a5c2a5a --- /dev/null +++ b/packages/core/src/models/notification.ts @@ -0,0 +1,147 @@ +import { $ } from 'minato' +import { Context } from '@satorijs/core' +import { Login } from '../types' +import { genId } from '@satorijs/plugin-im-utils' +import { Notification, Channel, Guild, Friend } from '../types' + +declare module '../types' { + namespace Notification { + interface Type { + 'new-friendship': Friend + 'new-guild': Guild + 'member-kick': void + } + } +} + +export class NotificationData { + constructor(public ctx: Context) {} + + list = async (login: Login) => { + const result = await this.ctx.database + .select( + 'satori-im.notification', + { + user: { id: login.selfId }, + }, + { + settings: true, + guild: true, + } + ) + .execute() + return result + } + + request = async (login: Login, target: string, content: string, gid?: string) => { + const data: Notification = { + id: genId(), + createdAt: new Date().getTime(), + shouldReply: true, + selfId: login.selfId!, + content, + } + + let guild: Guild | undefined = undefined + + // HACK: cannot judge the case that row.guild.id is null or not. + const result = gid + ? await this.ctx.database + .select('satori-im.notification', (row) => + $.and($.eq(row.selfId, login.selfId!), $.eq(row.user.id, target)) + ) + .execute() + : await this.ctx.database + .select('satori-im.notification', (row) => + $.and( + $.eq(row.selfId, login.selfId!), + $.eq(row.user.id, target), + $.eq(row.guild.id, gid!) + ) + ) + .execute() + + this.ctx.logger.info(result) + + if (!result.length) { + const res = await this.ctx.database.create('satori-im.notification', { + ...data, + user: { $literal: { id: target } }, + guild: { $literal: { id: gid } }, + }) + + if (gid) { + guild = await this.ctx.im.data.guild.fetch(login, gid) + } + + const invType = data.guild ? 'guild-member' : 'friend' + this.ctx['im.event'].pushEvent({ + selfId: data.selfId!, + type: data.shouldReply ? `${invType}-request` : 'notification-added', + notification: res, + user: { id: target }, + _data: data, + }) // HACK: _data is not ideal. + } + } + + reply = async (login: Login, nid: string, reply: boolean) => { + const result = await this.ctx.database.get('satori-im.notification', { + id: nid, + shouldReply: true, + }) + if (!result.length) { + return new Error() + } + const notification = result[0] + const isGuild = !!notification.guild!.id + + let creation: any + // HACK: directly created a relation here + if (reply === true) { + this.ctx.database.transact(async (db) => { + if (reply) { + if (isGuild) { + await db.create('satori-im.member', { + user: { $connect: { id: login.selfId } }, + guild: { $connect: { id: notification.guild!.id } }, + createdAt: new Date().getTime(), + }) + } else { + creation = await db.create('satori-im.friend', { + id: genId(), + self: { id: login.selfId }, + target: { id: notification.selfId }, + createdAt: new Date().getTime(), + }) + const channel = await db.create('satori-im.channel', { + id: genId(), + type: Channel.Type.DIRECT, + friend: { id: creation.id }, + parentId: creation.id, + }) + await db.create('satori-im.channel.settings', { + user: { $literal: { id: login.selfId } }, + channel: { $literal: { id: channel.id } }, + }) + await db.create('satori-im.friend.settings', { + user: { $literal: { id: login.selfId } }, + friend: { $literal: { id: creation.id } }, + }) + await db.create('satori-im.friend.settings', { + user: { $literal: { id: notification.selfId } }, + friend: { $literal: { id: creation.id } }, + }) + } + } + await db.remove('satori-im.notification', { id: nid }) + this.ctx['im.event'].pushEvent({ + selfId: login.selfId!, + type: `${isGuild ? 'guild' : 'friend'}-updated`, + guild: isGuild ? (creation as Guild) : undefined, + friend: !isGuild ? (creation as Friend) : undefined, + }) + }) + } + } +} diff --git a/packages/core/src/models/user.ts b/packages/core/src/models/user.ts index 8a23ef0..690bf0b 100644 --- a/packages/core/src/models/user.ts +++ b/packages/core/src/models/user.ts @@ -1,34 +1,76 @@ +import { $ } from 'minato' import { Context } from '@satorijs/core' -import { User } from '../types' +import { ChunkOptions, Login, User } from '../types' export class UserData { constructor(public ctx: Context) {} + + fetch = async (login: Login, uid: string): Promise => { + const result = await this.ctx.database.select('satori-im.user', { id: uid }).execute() + return result[0] } - async fetch(uid: string): Promise { - const result = await this.ctx.database.get('satori-im.user', uid, [ - 'id', - 'name', - 'avatar', - 'nick', - ]) + fetchSettings = async (login: Login): Promise => { + const result = await this.ctx.database + .select('satori-im.user.preferences', { user: { id: login.selfId } }) + .execute() return result[0] } - async update(data: { uid: string } & Partial): Promise { - const result = await this.ctx.database.set('satori-im.user', data.uid, (row) => ({ + update = async (login: Login, nick: string) => { + const result = await this.ctx.database.set('satori-im.user', { id: login.selfId }, (row) => ({ + nick: nick, + })) + if (!result.modified) { + throw new Error() + } + } + + _update = async (uid: string, data: { nick?: string; avatarUrl?: string }) => { + const result = await this.ctx.database.set('satori-im.user', { id: uid }, (row) => ({ nick: data.nick, - avatar: data.avatar, + avatar: data.avatarUrl, })) if (!result.modified) { throw new Error() } + + // FIXME: update event + // this.ctx['im.event'].pushEvent({ + // type: 'user-updated', + // selfId: uid, + // guild: { id: gid, name } as Guild, + // }) } - async softDel(uid: string): Promise {} + search = async ( + login: Login, + keyword: string, + options: ChunkOptions & { except: Array } + ): Promise> => { + const keywordRegex = new RegExp(`${keyword}`, 'i') + let query = this.ctx.database + .select('satori-im.user') + .where((row) => + $.and( + $.or( + $.regex(row.name, keywordRegex), + $.regex(row.nick, keywordRegex), + $.eq(row.id, keyword) + ), + ...options.except.map((value) => $.ne(row.id, value)) + ) + ) - // TODO: waiting for implementation. - async search(keyword: string): Promise> { - return this.ctx.database.get('satori-im.user', {}) + if (options.cursor === '#begin') { + query = query.orderBy('id', 'asc') + } else if (options.direction === 'before') { + query = query.where((row) => $.lt(row.id, options.cursor)).orderBy('id', 'desc') + } else if (options.direction === 'after') { + query = query.where((row) => $.gt(row.id, options.cursor)).orderBy('id', 'asc') + } else { + throw Error() + } + return query.limit(options.limit).execute() } } From a288604c7f953967c0cde298d1f10abd47bebe70 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:11:56 +0800 Subject: [PATCH 21/50] chore: type declarations --- packages/core/src/types.ts | 171 ++++++++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 30 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1296f22..5f6f279 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,9 +1,11 @@ import { Universal } from '@satorijs/core' +import { Row } from 'minato' declare module 'minato' { interface Tables { 'satori-im.channel.settings': Channel.Settings 'satori-im.channel': Channel + 'satori-im.friend.settings': Friend.Settings 'satori-im.friend': Friend 'satori-im.guild.settings': Guild.Settings 'satori-im.guild': Guild @@ -12,62 +14,133 @@ declare module 'minato' { 'satori-im.message.settings': Message.Settings 'satori-im.message.test': Message 'satori-im.role': Role + 'satori-im.user.preferences': User.Preferences + 'satori-im.user.auth': User.Auth 'satori-im.user.settings': User.Settings 'satori-im.user': User 'satori-im.notification': Notification + 'satori-im.notification.settings': Notification.Settings } } -declare module '@satorijs/protocol' { - interface Message { - sid?: bigint +export function extractSettings( + row: Row +): Row.Cell | undefined { + if (row.settings && row.settings.length > 0) { + return row.settings[0] } + return undefined } export interface Login extends Universal.Login { - user: User - updateAt: number + clientId?: string + selfId?: string + token: string + updateAt?: number + expiredAt?: number } export interface Notification { - self: User - type: Notification.Types - user: User - guild: Guild + id: string + selfId: string + shouldReply: boolean + // type: Notification.Type + createdAt?: number + user?: User + guild?: Guild content?: string + settings?: Array } export namespace Notification { - export enum Types { - NOT = 0, - REQ = 1, + export interface Type { + announcement: void + 'bump-version': void + } + + export interface Settings { + self: Notification + user: User + read: boolean } } -export type Event = Universal.Event +export type EventName = Universal.EventName | 'notification-added' +export interface Event extends Universal.Event { + type: EventName + channel?: Channel + friend?: Friend + guild?: Guild + member?: Member + message?: Message + notification?: Notification + role?: Role + user?: User +} export interface User extends Universal.User { - password?: string + settings?: Array + roles?: Array + members?: Array + deleted?: boolean } export namespace User { export interface Settings { - origin: User + user: User target: User level: NotifyLevels } + + export type Payload = Omit + + export interface Auth { + user: User + password: string + } + + export interface Preferences { + user: User + } } export interface Friend { id: string - origin: User + self: User target: User - group: string - pinned: boolean - nick?: string + createdAt?: number + deleted?: boolean + settings?: Array + channel?: Channel + messages?: Array } -export interface Guild extends Universal.Guild {} +export namespace Friend { + export interface Settings { + user: User + friend: Friend + pinned: boolean + group?: string + nick?: string + } + + export type Payload = Omit & { + user: User + nick?: string + level: NotifyLevels + pinned: boolean + group?: string + } +} + +export interface Guild extends Universal.Guild { + members?: Array + roles?: Array + settings?: Array + channels?: Array + createdAt?: number + deleted?: boolean +} export namespace Guild { export interface Settings { @@ -76,19 +149,33 @@ export namespace Guild { group: string pinned: boolean } + export type Payload = Omit & { + group?: string + pinned: boolean + channels: Array + } } export interface Member extends Universal.GuildMember { guild: Guild user: User + createdAt?: number } +// HACK: related to member instead of user. export interface Role extends Universal.GuildRole { - user: User + gid: string + users?: Array guild: Guild } -export interface Channel extends Universal.Channel {} +export interface Channel extends Universal.Channel { + friend?: Friend + guild?: Guild + deleted?: boolean + settings?: Array + messages?: Array +} export namespace Channel { export interface Settings { @@ -97,14 +184,26 @@ export namespace Channel { level: NotifyLevels nick?: string pinned: boolean - lastRead: string + lastRead: number } + + export type Payload = Omit & { + level: NotifyLevels + nick?: string + pinned: boolean + lastRead?: number + } + + export import Type = Universal.Channel.Type } export interface Message extends Universal.Message { - sid?: bigint // message sync id. - // type: Message.Type - flag: number + sid?: string // message sync id. + user?: User + member?: Member + channel: Channel + quote?: Message + deleted?: boolean } export namespace Message { @@ -119,19 +218,31 @@ export namespace Message { FINAL = 4, } - /** @deprecated */ - export enum Types { - PLAIN = 1, - } + export type Direction = 'before' | 'after' // 序列化 // 创建 Message } +export type IdOnly = Pick + export enum NotifyLevels { BLOCKED = 0, SILENT = 1, NORMAL = 2, IMPORTANT = 3, } + +export interface ChunkOptions { + direction: 'before' | 'after' + cursor: string + limit: number + offset?: number // used when skipping pages. +} + +export interface ChunkData { + current: number + total: number + data: Array<{ id: string }> +} From d7bed93e7e97d487e93a793bf9507eb3224ede52 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:15:34 +0800 Subject: [PATCH 22/50] chore: tweaks --- packages/im/package.json | 7 +------ packages/im/tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/im/package.json b/packages/im/package.json index 498b4b4..f700601 100644 --- a/packages/im/package.json +++ b/packages/im/package.json @@ -29,12 +29,7 @@ "im" ], "cordis": { - "service": { - "required": [ - "webui", - "im" - ] - } + "service": {} }, "peerDependencies": { "@cordisjs/plugin-webui": "^0.1.12", diff --git a/packages/im/tsconfig.json b/packages/im/tsconfig.json index e193a11..fd164e6 100644 --- a/packages/im/tsconfig.json +++ b/packages/im/tsconfig.json @@ -5,6 +5,6 @@ "outDir": "lib", }, "include": [ - "src", + "src" ], -} \ No newline at end of file +} \ No newline at end of file From f4df8c0464ff13da2151751a32d762ebf477d402 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:17:07 +0800 Subject: [PATCH 23/50] fix(im): change the logic of ImEntry.unicast --- packages/im/src/entry.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/im/src/entry.ts b/packages/im/src/entry.ts index f7b7dfe..a472c16 100644 --- a/packages/im/src/entry.ts +++ b/packages/im/src/entry.ts @@ -1,5 +1,5 @@ -import { Context, Service } from 'cordis' -import { Client, Entry } from '@cordisjs/plugin-webui' +import { Context, Service } from '@satorijs/core' +import { Client, Entry, Events } from '@cordisjs/plugin-webui' export class ImEntry extends Entry { constructor( @@ -13,7 +13,10 @@ export class ImEntry extends Entry { unicast(clientId: string, data: any) { const client = this.ctx.webui.clients[clientId] if (client) { - const payload = { type: 'entry:update', body: { id: this.id, data } } + const payload = { + type: 'entry:update', + body: { id: this.id, data: { ...this.data?.(client), ...data } }, + } client.socket.send(JSON.stringify(payload)) } } @@ -36,10 +39,10 @@ export class ImEntry extends Entry { } export class EntryService extends Service { - static inject = ['webui', 'server'] + static inject = ['webui', 'server', 'im', 'im.auth'] public entry: ImEntry - constructor(ctx: Context) { + constructor(public ctx: Context) { super(ctx, 'im.entry') this.entry = new ImEntry( ctx, From 5efc5e889067def91de7a71fa3bfe54ef407e904 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:18:40 +0800 Subject: [PATCH 24/50] refa: move ctx.webui here --- packages/im/src/entry.ts | 17 +++ packages/im/src/index.ts | 261 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 271 insertions(+), 7 deletions(-) diff --git a/packages/im/src/entry.ts b/packages/im/src/entry.ts index a472c16..b0b88ef 100644 --- a/packages/im/src/entry.ts +++ b/packages/im/src/entry.ts @@ -57,4 +57,21 @@ export class EntryService extends Service { }) ) } + + // Only supports v1 apis. + addListenerWithAuth(event: K, callback: any): void { + this.ctx.webui.addListener(event, async (data: any): Promise => { + try { + const decode = await this.ctx['im.auth']._verifyToken(data.login.token) + data.login.clientId = decode.clientId + data.login.selfId = decode.user!.id + return callback(data) + } catch (e) { + if (e) { + this.ctx['im.auth'].logout(data.token) + throw e + } + } + }) + } } diff --git a/packages/im/src/index.ts b/packages/im/src/index.ts index fa87bb9..13d599b 100644 --- a/packages/im/src/index.ts +++ b/packages/im/src/index.ts @@ -1,28 +1,275 @@ -import { Context, Schema, Service } from '@satorijs/core' -import {} from '@satorijs/plugin-server' -import { Client } from '@cordisjs/plugin-webui' -import { ImTypes } from '@satorijs/plugin-im' +import { Context, Schema } from '@satorijs/core' +import ImService, { Im } from '@satorijs/plugin-im' import { EntryService } from './entry' +import { v1Wrapper } from '@satorijs/plugin-im-utils' export interface Data { serverUrl: string - eventChan: ImTypes.Event + eventChan: Im.Event } export const name = 'im' declare module 'cordis' { interface Context { + im: ImService 'im.entry': EntryService } } +declare module '@cordisjs/plugin-webui' { + interface Events { + 'im/v1/login'(data: { name: string; password: string }): Promise + 'im/v1/login-add'(data: { login: Im.Login }): Promise + 'im/v1/login-change'(data: { uid: string; status: number }): Promise + 'im/v1/login-remove'(data: { login: Im.Login }): Promise + 'im/v1/register'(data: { name: string; password: string }): Promise + 'im/v1/validate-name'(data: { name: string }): Promise + + 'im/v1/search/user'(data: { + login: Im.Login + keyword: string + options: Im.ChunkOptions & { except: string[] } + }): Promise> + 'im/v1/search/guild'(data: { + login: Im.Login + keyword: string + options: Im.ChunkOptions & { except: string[] } + }): Promise> + 'im/v1/search/message'(data: { + login: Im.Login + keyword: string + options: Im.ChunkOptions & { except: string[] } + }): Promise> + 'im/v1/search/friend'(data: { + login: Im.Login + keyword: string + options: Im.ChunkOptions & { except: string[] } + }): Promise> + + 'im/v1/user/fetch'(data: { login: Im.Login; uid: string }): Promise + 'im/v1/user/update'(data: { login: Im.Login; nick: string; password: string }): Promise + 'im/v1/user/remove'(data: { login: Im.Login }): Promise + + 'im/v1/friend/fetch'(data: { login: Im.Login; fid: string }): Promise + 'im/v1/friend/fetch-all'(data: { login: Im.Login }): Promise> + 'im/v1/friend/remove'(data: { login: Im.Login; fid: string }): Promise + + 'im/v1/guild/fetch'(data: { login: Im.Login; gid: string }): Promise + 'im/v1/guild/fetch-all'(data: { login: Im.Login }): Promise> + 'im/v1/guild/create'(data: { + login: Im.Login + name: string + options: { inviteUsers?: string[]; initialChannelName?: string } + }): Promise + 'im/v1/guild/update'(data: { login: Im.Login; gid: string; name: string }): Promise + 'im/v1/guild/remove'(data: { login: Im.Login; gid: string }): Promise + 'im/v1/guild-member/fetch'(data: { + login: Im.Login + gid: string + uid: string + }): Promise + 'im/v1/guild-member/fetch-all'(data: { + login: Im.Login + gid: string + }): Promise> + 'im/v1/guild-member/update'(data: { + login: Im.Login + gid: string + uid?: string + name?: string + }): Promise + 'im/v1/guild-member/kick'(data: { login: Im.Login; gid: string; uid: string }): Promise + 'im/v1/guild-role/fetch'(data: { login: Im.Login; gid: string; rid: string }): Promise + 'im/v1/guild-role/fetch-all'(data: { login: Im.Login; gid: string }): Promise> + 'im/v1/guild-role/create'(data: { login: Im.Login; uid: string; gid: string }): Promise + 'im/v1/guild-role/update'(data: { login: Im.Login; rid: string }): Promise + 'im/v1/guild-role/remove'(data: { login: Im.Login; rid: string }): Promise + + 'im/v1/channel/fetch'(data: { login: Im.Login; cid: string }): Promise + 'im/v1/channel/create'(data: { + login: Im.Login + gid: string + name: string + }): Promise + 'im/v1/channel/update'(data: { login: Im.Login; cid: string; name: string }): Promise + 'im/v1/channel/remove'(data: { login: Im.Login; cid: string }): Promise + 'im/v1/channel/update-settings'(data: { login: Im.Login; cid: string }): Promise + 'im/v1/channel/recent'(data: { login: Im.Login }): Promise> + + 'im/v1/message/fetch'(data: { + login: Im.Login + cid: string + endpoint: number + dir?: 'before' | 'after' + limit?: number + }): Promise> + 'im/v1/message/create'(data: { login: Im.Login; message: Im.Message }): Promise + 'im/v1/message/update'(data: { login: Im.Login; message: Im.Message }): Promise + 'im/v1/message/recall'(data: { login: Im.Login; cid: string; id: string }): Promise + 'im/v1/message/unread-count'(data: { login: Im.Login; cid: string }): Promise + + 'im/v1/notification/fetch-all'(data: { login: Im.Login }): Promise> + + 'im/v1/invitation/friend'(data: { + login: Im.Login + target: string + content: string + }): Promise + 'im/v1/invitation/member'(data: { + login: Im.Login + target: string + content: string + gid: string + }): Promise + 'im/v1/invitation/reply'(data: { login: Im.Login; nid: string; reply: boolean }): Promise + + 'im/v1/file-upload'(data: { login: Im.Login; b64: string }): Promise + 'im/v1/avatar-upload'(data: { login: Im.Login; b64: string; gid?: string }): Promise + } +} + export interface Config {} -export const inject = ['im.event'] +export const inject = ['webui', 'server', 'im', 'im.event', 'im.auth', 'im.data'] export const Config: Schema = Schema.object({}) -export default function apply(ctx: Context) { +export function apply(ctx: Context) { ctx.plugin(EntryService) + + ctx.inject(['im.entry'], (ctx) => { + ctx.webui.addListener('im/v1/login', async function (data: { name: string; password: string }) { + // @ts-ignore + const clientId = this['x-client-id'] + return ctx.im.auth.authenticate(data.name, data.password, clientId) + }) // HACK: type ignorance + ctx.webui.addListener('im/v1/login-add', async function (data: { login: Im.Login }) { + // @ts-ignore + data.login.clientId = this['x-client-id'] + return ctx.im.auth.authenticateWithToken(data.login) + }) // HACK: type ignorance + ctx.webui.addListener('im/v1/login-remove', v1Wrapper(ctx.im.auth.logout)) + ctx.webui.addListener('im/v1/register', v1Wrapper(ctx.im.auth.register)) + ctx.webui.addListener('im/v1/validate-name', v1Wrapper(ctx.im.auth.isNameUnique)) + + ctx['im.entry'].addListenerWithAuth( + 'im/v1/invitation/friend', + v1Wrapper(ctx.im.data.notification.request) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/invitation/member', + v1Wrapper(ctx.im.data.notification.request) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/invitation/reply', + v1Wrapper(ctx.im.data.notification.reply) + ) + + ctx['im.entry'].addListenerWithAuth('im/v1/channel/fetch', v1Wrapper(ctx.im.data.channel.fetch)) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/channel/create', + v1Wrapper(ctx.im.data.channel.create) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/channel/update', + v1Wrapper(ctx.im.data.channel.update) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/channel/remove', + v1Wrapper(ctx.im.data.channel.softDel) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/channel/update-settings', + v1Wrapper(ctx.im.data.channel.updateSettings) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/channel/recent', + v1Wrapper(ctx.im.data.channel.fetchSessionList) + ) + + ctx['im.entry'].addListenerWithAuth('im/v1/friend/fetch', v1Wrapper(ctx.im.data.friend.fetch)) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/friend/fetch-all', + v1Wrapper(ctx.im.data.friend.list) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/friend/remove', + v1Wrapper(ctx.im.data.friend.softDel) + ) + ctx['im.entry'].addListenerWithAuth('im/v1/search/friend', v1Wrapper(ctx.im.data.friend.search)) + + ctx['im.entry'].addListenerWithAuth('im/v1/guild/fetch', v1Wrapper(ctx.im.data.guild.fetch)) + ctx['im.entry'].addListenerWithAuth('im/v1/guild/fetch-all', v1Wrapper(ctx.im.data.guild.list)) + ctx['im.entry'].addListenerWithAuth('im/v1/guild/create', v1Wrapper(ctx.im.data.guild.create)) + ctx['im.entry'].addListenerWithAuth('im/v1/guild/update', v1Wrapper(ctx.im.data.guild.update)) + ctx['im.entry'].addListenerWithAuth('im/v1/search/guild', v1Wrapper(ctx.im.data.guild.search)) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-member/fetch', + v1Wrapper(ctx.im.data.guild.Member.fetch) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-member/fetch-all', + v1Wrapper(ctx.im.data.guild.Member.list) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-member/update', + v1Wrapper(ctx.im.data.guild.Member.update) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-member/kick', + v1Wrapper(ctx.im.data.guild.Member.kick) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-role/fetch', + v1Wrapper(ctx.im.data.guild.Role.fetch) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-role/fetch-all', + v1Wrapper(ctx.im.data.guild.Role.list) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-role/create', + v1Wrapper(ctx.im.data.guild.Role.create) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-role/update', + v1Wrapper(ctx.im.data.guild.Role.update) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/guild-role/remove', + v1Wrapper(ctx.im.data.guild.Role.hardDel) + ) + + ctx['im.entry'].addListenerWithAuth('im/v1/message/fetch', v1Wrapper(ctx.im.data.message.fetch)) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/message/create', + v1Wrapper(ctx.im.data.message.create) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/message/update', + v1Wrapper(ctx.im.data.message.update) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/message/recall', + v1Wrapper(ctx.im.data.message.softDel) + ) + ctx['im.entry'].addListenerWithAuth( + 'im/v1/message/unread-count', + v1Wrapper(ctx.im.data.message.getUnreadCount) + ) + + ctx['im.entry'].addListenerWithAuth( + 'im/v1/notification/fetch-all', + v1Wrapper(ctx.im.data.notification.list) + ) + + ctx['im.entry'].addListenerWithAuth('im/v1/user/fetch', v1Wrapper(ctx.im.data.user.fetch)) + ctx['im.entry'].addListenerWithAuth('im/v1/user/update', v1Wrapper(ctx.im.data.user.update)) + ctx['im.entry'].addListenerWithAuth('im/v1/search/user', v1Wrapper(ctx.im.data.user.search)) + + ctx['im.entry'].addListenerWithAuth('im/v1/file-upload', v1Wrapper(ctx.im.data.writeFile)) + + ctx['im.entry'].addListenerWithAuth('im/v1/avatar-upload', v1Wrapper(ctx.im.data.writeAvatar)) + }) } From afdc7437fdac87f995a8fbc1ed9e5561c850c31e Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:22:28 +0800 Subject: [PATCH 25/50] refa: reduce attributes --- packages/im/client/shared/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/im/client/shared/index.ts b/packages/im/client/shared/index.ts index e96c733..3b089bd 100644 --- a/packages/im/client/shared/index.ts +++ b/packages/im/client/shared/index.ts @@ -1,14 +1,12 @@ -import { useStorage } from '@cordisjs/client' -import { ImTypes } from '@satorijs/plugin-im' +import { Dict, useStorage } from '@cordisjs/client' +import type Window from '../components/scene' interface SharedConfig { - shouldLogin: boolean - currentUser: ImTypes.User | null + token: string } const shared = useStorage('im', 1, () => ({ - shouldLogin: true, - currentUser: null, + token: '', })) export default shared From a6190cde33cd8d58a02d6fe66f1c340c769662fa Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:22:53 +0800 Subject: [PATCH 26/50] chore: tweak --- packages/utils/package.json | 38 ++++++++++++++++++++++++++++++++++++ packages/utils/tsconfig.json | 10 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 packages/utils/package.json create mode 100644 packages/utils/tsconfig.json diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000..660603e --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,38 @@ +{ + "name": "@satorijs/plugin-im-utils", + "description": "Utils of Satori IM", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/im" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "chat", + "im", + "database" + ], + "devDependencies": { + "@types/uuid": "^10.0.0" + }, + "dependencies": { + "cordis": "^3.18.0", + "uuid": "^10.0.0" + } +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000..fd164e6 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + }, + "include": [ + "src" + ], +} \ No newline at end of file From f849d564a5ed6e2be80e787aee931233c3e6e1f2 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:23:18 +0800 Subject: [PATCH 27/50] feat(webui): add icons --- packages/im/client/icons/add-square.vue | 9 +++++ packages/im/client/icons/bookmark.vue | 9 +++++ packages/im/client/icons/check.vue | 9 +++++ packages/im/client/icons/close.vue | 9 +++++ packages/im/client/icons/ellipsis-v.vue | 9 +++++ packages/im/client/icons/emoji/blank.vue | 9 +++++ .../client/icons/{ => emoji}/smile-wink.vue | 0 packages/im/client/icons/folder.vue | 9 +++++ packages/im/client/icons/inbox.vue | 9 +++++ packages/im/client/icons/index.ts | 34 ++++++++++++++++--- packages/im/client/icons/message.vue | 9 +++++ packages/im/client/icons/settings-gear.vue | 9 +++++ packages/im/client/icons/settings-slider.vue | 9 +++++ .../icons/{setting.vue => settings.vue} | 0 14 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 packages/im/client/icons/add-square.vue create mode 100644 packages/im/client/icons/bookmark.vue create mode 100644 packages/im/client/icons/check.vue create mode 100644 packages/im/client/icons/close.vue create mode 100644 packages/im/client/icons/ellipsis-v.vue create mode 100644 packages/im/client/icons/emoji/blank.vue rename packages/im/client/icons/{ => emoji}/smile-wink.vue (100%) create mode 100644 packages/im/client/icons/folder.vue create mode 100644 packages/im/client/icons/inbox.vue create mode 100644 packages/im/client/icons/message.vue create mode 100644 packages/im/client/icons/settings-gear.vue create mode 100644 packages/im/client/icons/settings-slider.vue rename packages/im/client/icons/{setting.vue => settings.vue} (100%) diff --git a/packages/im/client/icons/add-square.vue b/packages/im/client/icons/add-square.vue new file mode 100644 index 0000000..15aa972 --- /dev/null +++ b/packages/im/client/icons/add-square.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/bookmark.vue b/packages/im/client/icons/bookmark.vue new file mode 100644 index 0000000..d4bcef8 --- /dev/null +++ b/packages/im/client/icons/bookmark.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/check.vue b/packages/im/client/icons/check.vue new file mode 100644 index 0000000..fc3ed90 --- /dev/null +++ b/packages/im/client/icons/check.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/close.vue b/packages/im/client/icons/close.vue new file mode 100644 index 0000000..2f4a170 --- /dev/null +++ b/packages/im/client/icons/close.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/ellipsis-v.vue b/packages/im/client/icons/ellipsis-v.vue new file mode 100644 index 0000000..7055f43 --- /dev/null +++ b/packages/im/client/icons/ellipsis-v.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/emoji/blank.vue b/packages/im/client/icons/emoji/blank.vue new file mode 100644 index 0000000..f96fa59 --- /dev/null +++ b/packages/im/client/icons/emoji/blank.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/smile-wink.vue b/packages/im/client/icons/emoji/smile-wink.vue similarity index 100% rename from packages/im/client/icons/smile-wink.vue rename to packages/im/client/icons/emoji/smile-wink.vue diff --git a/packages/im/client/icons/folder.vue b/packages/im/client/icons/folder.vue new file mode 100644 index 0000000..d3c682a --- /dev/null +++ b/packages/im/client/icons/folder.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/inbox.vue b/packages/im/client/icons/inbox.vue new file mode 100644 index 0000000..d520bc6 --- /dev/null +++ b/packages/im/client/icons/inbox.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/index.ts b/packages/im/client/icons/index.ts index 9c2ed31..a79cd07 100644 --- a/packages/im/client/icons/index.ts +++ b/packages/im/client/icons/index.ts @@ -1,20 +1,31 @@ import { icons, IconSquareCheck } from '@cordisjs/client' +import AddSquare from './add-square.vue' import Ban from './ban.vue' import Bell from './bell.vue' +import Bookmark from './bookmark.vue' +import Blank from './emoji/blank.vue' +import Check from './check.vue' +import Close from './close.vue' import CircleCheck from './circle-check.vue' import Copy from './copy.vue' import Edit from './edit.vue' +import EllipsisV from './ellipsis-v.vue' import Envelope from './envelope.vue' +import Folder from './folder.vue' import Group from './group.vue' import Image from './image.vue' +import Inbox from './inbox.vue' import Chat from './chat.vue' +import Message from './message.vue' import Minus from './minus.vue' -import SmileWink from './smile-wink.vue' +import SmileWink from './emoji/smile-wink.vue' import Pin from './pin.vue' import Quote from './quote.vue' import Recall from './recall.vue' import Refresh from './refresh.vue' -import Setting from './setting.vue' +import SettingsGear from './settings-gear.vue' +import SettingsSlider from './settings-slider.vue' +import Settings from './settings.vue' import Share from './share.vue' import TrashCan from './trash-can.vue' @@ -23,28 +34,41 @@ import TextChannel from './channel/text.vue' import Bold from './editor/bold.vue' import Italic from './editor/italic.vue' +icons.register('im:add-square', AddSquare) icons.register('im:ban', Ban) icons.register('im:bell', Bell) +icons.register('im:bookmark', Bookmark) icons.register('im:chat', Chat) -icons.register('im:checked', CircleCheck) +icons.register('im:check', Check) +icons.register('im:check-round', CircleCheck) +icons.register('im:close', Close) icons.register('im:copy', Copy) icons.register('im:edit', Edit) +icons.register('im:ellipsis-vertical', EllipsisV) icons.register('im:envelope', Envelope) -icons.register('im:emoji', SmileWink) +icons.register('im:folder', Folder) icons.register('im:group', Group) icons.register('im:image', Image) +icons.register('im:inbox', Inbox) +icons.register('im:message', Message) icons.register('im:minus', Minus) icons.register('im:pin', Pin) icons.register('im:quote', Quote) icons.register('im:readed', IconSquareCheck) icons.register('im:recall', Recall) icons.register('im:refresh', Refresh) -icons.register('im:setting', Setting) +icons.register('im:settings', Settings) +icons.register('im:settings-gear', SettingsGear) +icons.register('im:settings-slider', SettingsSlider) icons.register('im:share', Share) icons.register('im:trash-can', TrashCan) icons.register('im:text-channel', TextChannel) +// emoji +icons.register('im:emoji', SmileWink) +icons.register('im:personnel', Blank) + // editor icons.register('editor:bold', Bold) icons.register('editor:italic', Italic) diff --git a/packages/im/client/icons/message.vue b/packages/im/client/icons/message.vue new file mode 100644 index 0000000..33e8ad4 --- /dev/null +++ b/packages/im/client/icons/message.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/settings-gear.vue b/packages/im/client/icons/settings-gear.vue new file mode 100644 index 0000000..0ae84cc --- /dev/null +++ b/packages/im/client/icons/settings-gear.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/settings-slider.vue b/packages/im/client/icons/settings-slider.vue new file mode 100644 index 0000000..79d13e6 --- /dev/null +++ b/packages/im/client/icons/settings-slider.vue @@ -0,0 +1,9 @@ + diff --git a/packages/im/client/icons/setting.vue b/packages/im/client/icons/settings.vue similarity index 100% rename from packages/im/client/icons/setting.vue rename to packages/im/client/icons/settings.vue From f00715f2b07c5a78282d8ffbe967526e3254acff Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:25:32 +0800 Subject: [PATCH 28/50] chore: delete uselessness --- .../im/client/components/tab/tab-item.vue | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 packages/im/client/components/tab/tab-item.vue diff --git a/packages/im/client/components/tab/tab-item.vue b/packages/im/client/components/tab/tab-item.vue deleted file mode 100644 index 4126114..0000000 --- a/packages/im/client/components/tab/tab-item.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - From 16edbe804bf26ff1a3a931f63fce238fc7530ee4 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:26:02 +0800 Subject: [PATCH 29/50] feat(webui): right aside logic --- packages/im/client/app/aside/aside-guild.vue | 217 ++++++++++++++++++ packages/im/client/app/aside/aside-user.vue | 104 +++++++++ packages/im/client/app/aside/aside.vue | 52 +++++ packages/im/client/app/aside/index.ts | 12 + .../im/client/app/aside/settings/collapse.vue | 43 ++++ .../im/client/app/aside/settings/form.vue | 11 + .../im/client/app/aside/settings/index.scss | 7 + .../im/client/app/aside/settings/index.ts | 14 ++ .../im/client/app/aside/settings/input.vue | 24 ++ .../im/client/app/aside/settings/selector.vue | 26 +++ .../im/client/app/aside/settings/switch.vue | 24 ++ 11 files changed, 534 insertions(+) create mode 100644 packages/im/client/app/aside/aside-guild.vue create mode 100644 packages/im/client/app/aside/aside-user.vue create mode 100644 packages/im/client/app/aside/aside.vue create mode 100644 packages/im/client/app/aside/index.ts create mode 100644 packages/im/client/app/aside/settings/collapse.vue create mode 100644 packages/im/client/app/aside/settings/form.vue create mode 100644 packages/im/client/app/aside/settings/index.scss create mode 100644 packages/im/client/app/aside/settings/index.ts create mode 100644 packages/im/client/app/aside/settings/input.vue create mode 100644 packages/im/client/app/aside/settings/selector.vue create mode 100644 packages/im/client/app/aside/settings/switch.vue diff --git a/packages/im/client/app/aside/aside-guild.vue b/packages/im/client/app/aside/aside-guild.vue new file mode 100644 index 0000000..3e32226 --- /dev/null +++ b/packages/im/client/app/aside/aside-guild.vue @@ -0,0 +1,217 @@ + + + diff --git a/packages/im/client/app/aside/aside-user.vue b/packages/im/client/app/aside/aside-user.vue new file mode 100644 index 0000000..bc2d24f --- /dev/null +++ b/packages/im/client/app/aside/aside-user.vue @@ -0,0 +1,104 @@ + + + diff --git a/packages/im/client/app/aside/aside.vue b/packages/im/client/app/aside/aside.vue new file mode 100644 index 0000000..d35c813 --- /dev/null +++ b/packages/im/client/app/aside/aside.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/im/client/app/aside/index.ts b/packages/im/client/app/aside/index.ts new file mode 100644 index 0000000..f09f8ca --- /dev/null +++ b/packages/im/client/app/aside/index.ts @@ -0,0 +1,12 @@ +import { Context } from '@cordisjs/client' +import settings from './settings' +import Aside from './aside.vue' + +export { default as AsideUser } from './aside-user.vue' +export { default as AsideGuild } from './aside-guild.vue' + +export default function (ctx: Context) { + ctx.app.component('im-aside', Aside) + + ctx.app.use(settings) +} diff --git a/packages/im/client/app/aside/settings/collapse.vue b/packages/im/client/app/aside/settings/collapse.vue new file mode 100644 index 0000000..cfea904 --- /dev/null +++ b/packages/im/client/app/aside/settings/collapse.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/im/client/app/aside/settings/form.vue b/packages/im/client/app/aside/settings/form.vue new file mode 100644 index 0000000..ef1b34a --- /dev/null +++ b/packages/im/client/app/aside/settings/form.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/im/client/app/aside/settings/index.scss b/packages/im/client/app/aside/settings/index.scss new file mode 100644 index 0000000..8577320 --- /dev/null +++ b/packages/im/client/app/aside/settings/index.scss @@ -0,0 +1,7 @@ +.im-settings { + font-size: 0.875rem; + min-height: 2.5rem; + font-weight: bold; + padding: 0.5rem 0.25rem; + box-sizing: border-box; +} diff --git a/packages/im/client/app/aside/settings/index.ts b/packages/im/client/app/aside/settings/index.ts new file mode 100644 index 0000000..232daf3 --- /dev/null +++ b/packages/im/client/app/aside/settings/index.ts @@ -0,0 +1,14 @@ +import { App } from 'vue' +import Collapse from './collapse.vue' +import Form from './form.vue' +import Input from './input.vue' +import Select from './selector.vue' +import Switch from './switch.vue' + +export default function (app: App) { + app.component('settings-collapse', Collapse) + app.component('settings-form', Form) + app.component('settings-input', Input) + app.component('settings-select', Select) + app.component('settings-switch', Switch) +} diff --git a/packages/im/client/app/aside/settings/input.vue b/packages/im/client/app/aside/settings/input.vue new file mode 100644 index 0000000..8be00bc --- /dev/null +++ b/packages/im/client/app/aside/settings/input.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/im/client/app/aside/settings/selector.vue b/packages/im/client/app/aside/settings/selector.vue new file mode 100644 index 0000000..f4aa563 --- /dev/null +++ b/packages/im/client/app/aside/settings/selector.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/im/client/app/aside/settings/switch.vue b/packages/im/client/app/aside/settings/switch.vue new file mode 100644 index 0000000..7e191a7 --- /dev/null +++ b/packages/im/client/app/aside/settings/switch.vue @@ -0,0 +1,24 @@ + + + + + From 4eff234fb72661aa539e745f04cafadf924191c4 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:27:38 +0800 Subject: [PATCH 30/50] refa: separate app logic from components --- packages/im/client/app/index.ts | 136 ++++++++++ packages/im/client/app/index.vue | 116 ++++++++ packages/im/client/components/aside/aside.vue | 22 -- packages/im/client/components/aside/index.ts | 6 - packages/im/client/components/index.ts | 37 ++- packages/im/client/components/index.vue | 61 ----- .../im/client/components/list/chat-list.vue | 254 ------------------ .../im/client/components/list/friend-item.vue | 6 - .../im/client/components/list/guild-item.vue | 6 - packages/im/client/components/list/index.ts | 8 - .../client/components/list/message-item.vue | 60 ----- .../client/components/list/message-list.vue | 52 ---- .../client/components/list/session-item.vue | 0 .../im/client/components/role/login-form.vue | 116 -------- .../im/client/components/windowed/index.ts | 6 - .../components/windowed/message-window.vue | 51 ---- 16 files changed, 276 insertions(+), 661 deletions(-) create mode 100644 packages/im/client/app/index.ts create mode 100644 packages/im/client/app/index.vue delete mode 100644 packages/im/client/components/aside/aside.vue delete mode 100644 packages/im/client/components/aside/index.ts delete mode 100644 packages/im/client/components/index.vue delete mode 100644 packages/im/client/components/list/chat-list.vue delete mode 100644 packages/im/client/components/list/friend-item.vue delete mode 100644 packages/im/client/components/list/guild-item.vue delete mode 100644 packages/im/client/components/list/index.ts delete mode 100644 packages/im/client/components/list/message-item.vue delete mode 100644 packages/im/client/components/list/message-list.vue delete mode 100644 packages/im/client/components/list/session-item.vue delete mode 100644 packages/im/client/components/role/login-form.vue delete mode 100644 packages/im/client/components/windowed/index.ts delete mode 100644 packages/im/client/components/windowed/message-window.vue diff --git a/packages/im/client/app/index.ts b/packages/im/client/app/index.ts new file mode 100644 index 0000000..0fbf032 --- /dev/null +++ b/packages/im/client/app/index.ts @@ -0,0 +1,136 @@ +import { computed, ref, Ref } from 'vue' +import { Context, send } from '@cordisjs/client' +import type { Im } from '@satorijs/plugin-im' +import aside, { AsideGuild, AsideUser } from './aside' +import navigation from './navigation' +import messenger, { ChatScene } from './messenger' +import user, { MyInfoScene, GlobalSearchScene } from './user' +import Scene, { Native } from './scene' + +export { default as Scene, Native } from './scene' +export { default as ImApp } from './index.vue' + +declare module '@cordisjs/client' { + interface ActionContext { + 'chat-session': void + 'user-settings': void + } +} + +declare module './scene' { + interface Scenes { + 'chat-friend': { + messages?: Ref> + friend: Im.Friend.Payload + channel: Im.Channel + } + 'chat-guild': { messages?: Ref>; guild: Im.Guild; channel: Im.Channel } + } +} + +export const inject = ['im.client'] + +export default function (ctx: Context) { + ctx.plugin(user) + ctx.plugin(aside) + ctx.plugin(navigation) + ctx.plugin(messenger) + + ctx.inject(['im.client'], (injected) => { + const chat = injected['im.client'] + + Scene.register('chat-friend', ChatScene, AsideUser, { + onInit: async (scene) => { + const messageData = await chat.getMessageData(scene.channel) + scene.messages = ref(messageData.data) + + scene.unread = computed({ + get: () => messageData.unread, + set: (value) => { + send('im/v1/channel/update-settings', { + login: chat.getLogin(), + cid: scene.channel.id, + }) + messageData.unread = value + }, + }) + + scene.pinned = computed({ + get: () => scene.friend.pinned || false, + set: (value) => { + scene.friend.pinned = value + // TODO: + // send('im/v1/friend/update-settings', { + + // }).catch(() => (scene.friend.pinned = !value)) + }, + }) + + const manualBrief = ref('') + scene.brief = computed({ + get: () => + manualBrief.value || + scene.messages?.value[scene.messages!.value.length - 1]?.content! || + '', + set: (value) => (manualBrief.value = value), + }) + }, + onMount: async (scene) => { + scene.unread = -1 // HACK: cannot set unread to 0 twice. + }, + onUnmount: async (scene) => { + scene.unread = 0 + }, + }) + Scene.register('chat-guild', ChatScene, AsideGuild, { + onInit: async (scene) => { + const messageData = await chat.getMessageData(scene.channel) + scene.messages = ref(messageData.data) + + scene.unread = computed({ + get: () => messageData.unread, + set: (value) => { + send('im/v1/channel/update-settings', { + login: chat.getLogin(), + cid: scene.channel.id, + }) + messageData.unread = value + }, + }) + + scene.pinned = computed({ + get: () => scene.guild.settings?.[0].pinned || false, + set: (value) => { + if (scene.guild.settings?.[0].pinned) { + scene.guild.settings[0].pinned = value + } + + // TODO: + // send('im/v1/friend/update-settings', { + + // }).catch(() => (scene.friend.pinned = !value)) + }, + }) + + const manualBrief = ref('') + scene.brief = computed({ + get: () => + manualBrief.value || + scene.messages?.value[scene.messages!.value.length - 1]?.content! || + '', + set: (value) => (manualBrief.value = value), + }) + }, + onMount: async (scene) => { + scene.unread = -1 // HACK: cannot set unread to 0 twice. + }, + onUnmount: async (scene) => { + scene.unread = 0 + }, + }) + Scene.register('edit-user', MyInfoScene) + Scene.register('search', GlobalSearchScene) + Scene.register('msg-box', Native.MsgBoxScene) + Scene.register('create-guild', Native.CreateGuildScene) + }) +} diff --git a/packages/im/client/app/index.vue b/packages/im/client/app/index.vue new file mode 100644 index 0000000..75d7e7d --- /dev/null +++ b/packages/im/client/app/index.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/packages/im/client/components/aside/aside.vue b/packages/im/client/components/aside/aside.vue deleted file mode 100644 index f9da90d..0000000 --- a/packages/im/client/components/aside/aside.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/packages/im/client/components/aside/index.ts b/packages/im/client/components/aside/index.ts deleted file mode 100644 index 601bdbf..0000000 --- a/packages/im/client/components/aside/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { App } from 'vue' -import Aside from './aside.vue' - -export default function (app: App) { - app.component('k-im-aside', Aside) -} diff --git a/packages/im/client/components/index.ts b/packages/im/client/components/index.ts index 5009bc2..3fb7b86 100644 --- a/packages/im/client/components/index.ts +++ b/packages/im/client/components/index.ts @@ -1,20 +1,31 @@ import { App } from 'vue' -import aside from './aside' +import { Context } from '@cordisjs/client' import role from './role' -import list from './list' +import stepper from './stepper' import tab from './tab' -import windowed from './windowed' +import Divider from './divider.vue' +import Essential from './essential.vue' +import { FilePicker } from './fs' +import Form from './form/form.vue' +import FormItem from './form/item.vue' +import More from './more.vue' +import Search from './search/global.vue' -export * from './aside' -export * from './role' -export * from './list' +export * from './fs' +export * from './stepper' export * from './tab' -export * from './windowed' +export * from './role' + +export default function apply(ctx: Context) { + ctx.app.component('file-picker', FilePicker) + ctx.app.component('essential', Essential) + ctx.app.component('more', More) + ctx.app.component('im-search', Search) + ctx.app.component('im-divider', Divider) + ctx.app.component('im-form', Form) + ctx.app.component('im-form-item', FormItem) -export default function (app: App) { - app.use(aside) - app.use(list) - app.use(tab) - app.use(role) - app.use(windowed) + ctx.app.use(tab) + ctx.app.use(stepper) + ctx.plugin(role) } diff --git a/packages/im/client/components/index.vue b/packages/im/client/components/index.vue deleted file mode 100644 index 4dc70c6..0000000 --- a/packages/im/client/components/index.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/packages/im/client/components/list/chat-list.vue b/packages/im/client/components/list/chat-list.vue deleted file mode 100644 index 45f399c..0000000 --- a/packages/im/client/components/list/chat-list.vue +++ /dev/null @@ -1,254 +0,0 @@ - - - diff --git a/packages/im/client/components/list/friend-item.vue b/packages/im/client/components/list/friend-item.vue deleted file mode 100644 index c2e0d1e..0000000 --- a/packages/im/client/components/list/friend-item.vue +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/packages/im/client/components/list/guild-item.vue b/packages/im/client/components/list/guild-item.vue deleted file mode 100644 index 9107677..0000000 --- a/packages/im/client/components/list/guild-item.vue +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/packages/im/client/components/list/index.ts b/packages/im/client/components/list/index.ts deleted file mode 100644 index a1b150d..0000000 --- a/packages/im/client/components/list/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { App } from 'vue' -import ChatList from './chat-list.vue' -import MessageList from './message-list.vue' - -export default function (app: App) { - app.component('k-im-chat-list', ChatList) - app.component('k-im-message-list', MessageList) -} diff --git a/packages/im/client/components/list/message-item.vue b/packages/im/client/components/list/message-item.vue deleted file mode 100644 index 82e48c2..0000000 --- a/packages/im/client/components/list/message-item.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/packages/im/client/components/list/message-list.vue b/packages/im/client/components/list/message-list.vue deleted file mode 100644 index 34e0ed1..0000000 --- a/packages/im/client/components/list/message-list.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/packages/im/client/components/list/session-item.vue b/packages/im/client/components/list/session-item.vue deleted file mode 100644 index e69de29..0000000 diff --git a/packages/im/client/components/role/login-form.vue b/packages/im/client/components/role/login-form.vue deleted file mode 100644 index f6e9472..0000000 --- a/packages/im/client/components/role/login-form.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/packages/im/client/components/windowed/index.ts b/packages/im/client/components/windowed/index.ts deleted file mode 100644 index fb7b2be..0000000 --- a/packages/im/client/components/windowed/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { App, defineComponent } from 'vue' -import MessageWindow from './message-window.vue' - -export default function (app: App) { - app.component('k-im-window-message', MessageWindow) -} diff --git a/packages/im/client/components/windowed/message-window.vue b/packages/im/client/components/windowed/message-window.vue deleted file mode 100644 index 214e56b..0000000 --- a/packages/im/client/components/windowed/message-window.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - From edf4254d44a2c1e9edad7b58bbd8ce38d888e22e Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:28:35 +0800 Subject: [PATCH 31/50] feat(webui): chat logic --- packages/im/client/app/messenger/chat.vue | 172 ++++++++++++++++++ packages/im/client/app/messenger/index.ts | 112 ++++++++++++ .../im/client/app/messenger/message-item.vue | 123 +++++++++++++ .../im/client/app/messenger/message/image.vue | 49 +++++ .../im/client/app/messenger/message/index.ts | 74 ++++++++ .../client/app/messenger/message/unknown.vue | 12 ++ 6 files changed, 542 insertions(+) create mode 100644 packages/im/client/app/messenger/chat.vue create mode 100644 packages/im/client/app/messenger/index.ts create mode 100644 packages/im/client/app/messenger/message-item.vue create mode 100644 packages/im/client/app/messenger/message/image.vue create mode 100644 packages/im/client/app/messenger/message/index.ts create mode 100644 packages/im/client/app/messenger/message/unknown.vue diff --git a/packages/im/client/app/messenger/chat.vue b/packages/im/client/app/messenger/chat.vue new file mode 100644 index 0000000..bbf7802 --- /dev/null +++ b/packages/im/client/app/messenger/chat.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/packages/im/client/app/messenger/index.ts b/packages/im/client/app/messenger/index.ts new file mode 100644 index 0000000..ec62b32 --- /dev/null +++ b/packages/im/client/app/messenger/index.ts @@ -0,0 +1,112 @@ +import { Context, send } from '@cordisjs/client' +import type { Im } from '@satorijs/plugin-im' +import transform from './message' +import MdImage from './message/image.vue' +import MdUnknown from './message/unknown.vue' + +export { default as ChatScene } from './chat.vue' + +declare module '@cordisjs/client' { + interface ActionContext { + message: Im.Message + } +} + +class Messenger { + private tempIdCounter: bigint = BigInt(0) + private static _instance: Messenger + + static get instance(): Messenger { + if (!Messenger._instance) { + this._instance = new Messenger() + } + return this._instance + } + + private constructor() {} + + rendMessage = transform + + genTempId(): string { + this.tempIdCounter += BigInt(1) + return this.tempIdCounter.toString() + } +} + +export const imMessenger = Messenger.instance + +export default function (ctx: Context) { + ctx.menu('message', [ + { + id: '.copy', + icon: 'im:copy', + label: '复制文本', + }, + { + id: 'quote', + icon: 'im:quote', + label: '回复', + }, + { + id: 'share', + icon: 'im:share', + label: '分享', + }, + { + id: 'select', + icon: 'im:check-round', + label: '多选', + }, + { + id: '@separator', + }, + { + id: '.recall', + icon: 'im:recall', + label: '撤回', + }, + { + id: '@separator', + }, + { + id: '.delete', + icon: 'delete', + label: '删除', + }, + ]) + + ctx.inject(['im.client'], (injected) => { + const chat = injected['im.client'] + injected.action('message.copy', ({ message }) => { + navigator.clipboard.writeText(message.content!) + }) + injected.action('message.quote', ({ message }) => { + // TODO: + }) + injected.action('message.share', ({ message }) => { + // TODO: + }) + injected.action('message.select', ({ message }) => { + // TODO: select mode + }) + injected.action('message.recall', { + hidden: ({ message }) => { + const me = chat.getLogin().user! + return !(message.user!.id === me.id) + }, + action: ({ message }) => { + send('im/v1/message/recall', { + login: chat.getLogin(), + cid: message.channel!.id, + id: message.id!, + }) + }, + }) + injected.action('message.delete', ({ message }) => { + // send('im/v1/message/settings') + }) + }) + + ctx.app.component('md-image', MdImage) + ctx.app.component('md-unknown', MdUnknown) +} diff --git a/packages/im/client/app/messenger/message-item.vue b/packages/im/client/app/messenger/message-item.vue new file mode 100644 index 0000000..a99df89 --- /dev/null +++ b/packages/im/client/app/messenger/message-item.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/image.vue b/packages/im/client/app/messenger/message/image.vue new file mode 100644 index 0000000..a296b7c --- /dev/null +++ b/packages/im/client/app/messenger/message/image.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/index.ts b/packages/im/client/app/messenger/message/index.ts new file mode 100644 index 0000000..b76d1a3 --- /dev/null +++ b/packages/im/client/app/messenger/message/index.ts @@ -0,0 +1,74 @@ +import { h, VNode, resolveComponent } from 'vue' +import { marked, Token, TokenizerExtensionFunction } from 'marked' + +export { Token, Tokens as TokenType } from 'marked' + +const tagRegex = /^<(\/?)([^!\s>/]+)([^>]*?)\s*(\/?)>$/ + +const component: TokenizerExtensionFunction = function (this, src) { + const match = src.match(/\/(\{[^}]*\});/) + if (match) { + return { + type: 'component', + raw: match[0], + text: match[1], + } as any + } + + return false +} + +export const tokenizer = (content: string): Token[] => + marked.lexer(content, { + extensions: { + inline: [component], + } as any, + }) + +export function renderer(tokens: Token[]): VNode[] { + return tokens.map(renderToken).filter(Boolean) +} + +export default function transform(content: string): VNode[] { + if (!content) return [h('div')] + return renderer(tokenizer(content)) +} + +function rendChilds(tokens: Token[]): VNode[] | undefined { + if (!tokens) { + console.log('no tokens') + return undefined + } + return tokens.map(renderToken).filter(Boolean) +} + +function renderToken(token: Token): VNode { + if (token.type === 'component') { + console.log(token) + const obj = JSON.parse(token.text) + const { name, ...args } = obj + if (!resolveComponent(name)) { + h('text', token.text) + } + return h(resolveComponent(name), { ...args }) + } else if (token.type === 'code') { + return h('code', { content: token.text + '\n' }) + } else if (token.type === 'paragraph') { + return h('p', {}, rendChilds(token.tokens!)) + } else if (token.type === 'image') { + return h(resolveComponent('md-image'), { src: token.href }) + } else if (token.type === 'blockquote') { + return h('blockquote', rendChilds(token.tokens!)) // TODO + } else if (token.type === 'text') { + return h('text', token.text) + } else if (token.type === 'em') { + return h('em', rendChilds(token.tokens!)) + } else if (token.type === 'strong') { + return h('b', rendChilds(token.tokens!)) + } else if (token.type === 'del') { + return h('del', rendChilds(token.tokens!)) + } else if (token.type === 'link') { + return h('a', { href: token.href }, rendChilds(token.tokens!)) + } + return h('p', token.raw) +} diff --git a/packages/im/client/app/messenger/message/unknown.vue b/packages/im/client/app/messenger/message/unknown.vue new file mode 100644 index 0000000..11807f9 --- /dev/null +++ b/packages/im/client/app/messenger/message/unknown.vue @@ -0,0 +1,12 @@ + + + From fb139fb059f8476501a43531b197c2457aac1240 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:29:57 +0800 Subject: [PATCH 32/50] feat(webui): left navigation logic --- .../im/client/app/navigation/friend-item.vue | 44 +++++ .../im/client/app/navigation/guild-item.vue | 71 +++++++++ packages/im/client/app/navigation/index.ts | 85 ++++++++++ packages/im/client/app/navigation/list.vue | 150 ++++++++++++++++++ .../im/client/app/navigation/session-item.vue | 58 +++++++ packages/im/client/app/scene/create-guild.vue | 62 ++++++++ 6 files changed, 470 insertions(+) create mode 100644 packages/im/client/app/navigation/friend-item.vue create mode 100644 packages/im/client/app/navigation/guild-item.vue create mode 100644 packages/im/client/app/navigation/index.ts create mode 100644 packages/im/client/app/navigation/list.vue create mode 100644 packages/im/client/app/navigation/session-item.vue create mode 100644 packages/im/client/app/scene/create-guild.vue diff --git a/packages/im/client/app/navigation/friend-item.vue b/packages/im/client/app/navigation/friend-item.vue new file mode 100644 index 0000000..e8b448f --- /dev/null +++ b/packages/im/client/app/navigation/friend-item.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/im/client/app/navigation/guild-item.vue b/packages/im/client/app/navigation/guild-item.vue new file mode 100644 index 0000000..352b194 --- /dev/null +++ b/packages/im/client/app/navigation/guild-item.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/im/client/app/navigation/index.ts b/packages/im/client/app/navigation/index.ts new file mode 100644 index 0000000..ce7e6cb --- /dev/null +++ b/packages/im/client/app/navigation/index.ts @@ -0,0 +1,85 @@ +import { Context } from '@cordisjs/client' +import Scene from '../scene' +export { default as ImNavigation } from './list.vue' + +declare module '@cordisjs/client' { + interface ActionContext { + 'new-chat': void + session: Scene.Prototype + } +} + +export default function (ctx: Context) { + ctx.menu('new-chat', [ + { + id: '.add', + icon: 'add', + label: '新好友/群聊', + }, + { + id: '.create-guild', + icon: 'im:group', + label: '创建群聊', + }, + ]) + + ctx.menu('session', [ + { + id: '.pin', + icon: 'im:pin', + label: '置顶', + }, + { + id: '.unpin', + icon: 'im:pin', + label: '取消置顶', + }, + { + id: '.readed', + icon: 'im:readed', + label: '设置已读', + }, + { + id: '@separator', + }, + { + id: '.close', + icon: 'close', + label: '关闭此会话', + }, + ]) + + // FIXME: 在执行 Scene.close 时,menu 组件似乎未销毁。 + // 这导致了 hidden 会在(大约)菜单消失时访问到一个为假值的 session。 + // 目前在 hidden 加入链式访问操作符,保证安全访问。 + ctx.action('session.pin', { + hidden: ({ session }) => { + return !!session?.pinned + }, + action: ({ session }) => { + session.pinned = true + }, + }) + ctx.action('session.unpin', { + hidden: ({ session }) => { + return !session?.pinned + }, + action: ({ session }) => { + session.pinned = false + }, + }) + ctx.action('session.readed', { + hidden: ({ session }) => { + return session?.id === Scene.current.value.id + }, + action: ({ session }) => { + session.unread = 0 + }, + }) + ctx.action('session.close', ({ session }) => { + // HACK: fake implementation. + // In fact, im just initializes sessions from the past 12 weeks. + + Scene.close(session.id!) + }) +} diff --git a/packages/im/client/app/navigation/list.vue b/packages/im/client/app/navigation/list.vue new file mode 100644 index 0000000..1d3b979 --- /dev/null +++ b/packages/im/client/app/navigation/list.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/packages/im/client/app/navigation/session-item.vue b/packages/im/client/app/navigation/session-item.vue new file mode 100644 index 0000000..3a94f6e --- /dev/null +++ b/packages/im/client/app/navigation/session-item.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/im/client/app/scene/create-guild.vue b/packages/im/client/app/scene/create-guild.vue new file mode 100644 index 0000000..113e72c --- /dev/null +++ b/packages/im/client/app/scene/create-guild.vue @@ -0,0 +1,62 @@ + + + From acd4d384c00f85f944f19371127de654616eca1c Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:34:13 +0800 Subject: [PATCH 33/50] feat(webui): scene module --- packages/im/client/app/scene/empty.vue | 3 + packages/im/client/app/scene/index.ts | 202 +++++++++++++++++++++++ packages/im/client/app/scene/loading.vue | 3 + packages/im/client/app/scene/msgbox.vue | 84 ++++++++++ 4 files changed, 292 insertions(+) create mode 100644 packages/im/client/app/scene/empty.vue create mode 100644 packages/im/client/app/scene/index.ts create mode 100644 packages/im/client/app/scene/loading.vue create mode 100644 packages/im/client/app/scene/msgbox.vue diff --git a/packages/im/client/app/scene/empty.vue b/packages/im/client/app/scene/empty.vue new file mode 100644 index 0000000..d71f88c --- /dev/null +++ b/packages/im/client/app/scene/empty.vue @@ -0,0 +1,3 @@ + diff --git a/packages/im/client/app/scene/index.ts b/packages/im/client/app/scene/index.ts new file mode 100644 index 0000000..754f781 --- /dev/null +++ b/packages/im/client/app/scene/index.ts @@ -0,0 +1,202 @@ +import { computed, Component, ref, Ref, MaybeRef } from 'vue' +import { Context, Dict, Service, send } from '@cordisjs/client' +import type { Im } from '@satorijs/plugin-im' + +import Empty from './empty.vue' +import Loading from './loading.vue' + +import CreateGuildScene from './create-guild.vue' +import MsgBoxScene from './msgbox.vue' + +interface Lifecycle { + onInit?: (scene: Scene[K]) => Promise + onMount?: (scene: Scene[K]) => Promise + onUnmount?: (scene: Scene[K]) => Promise +} + +type SceneDict = Dict< + { + component: Component + aside?: Component + handlers?: Lifecycle + }, + K +> + +let startId: number = 0 +const registered = {} as SceneDict + +export const Native = { + MsgBoxScene, + CreateGuildScene, +} + +// TODO: better type constraints. +export interface Scenes { + 'edit-user': { user?: Im.User } + 'create-guild': void + 'msg-box': { msgs?: Array } + search: void +} + +type Scene = { + [K in keyof Scenes]: Scene.Prototype & + (Scenes[K] extends object ? Scenes[K] : {}) & { + readonly lifecycles?: Lifecycle + readonly id: string + readonly type: keyof Scenes + readonly uid?: string + } +} + +namespace Scene { + type ToCreate = Omit + + export type ID = string + + export interface Prototype { + lifecycles?: Lifecycle + _data?: any + avatar?: string + brief?: MaybeRef + id?: ID + loaded?: boolean + pinned?: MaybeRef + subtitle?: string + title: MaybeRef + type?: keyof Scenes + uid?: string + unread?: MaybeRef + } + + export const current = computed(() => { + const topInstance = top() + if (!topInstance) { + return { ...({} as Prototype), component: Empty, aside: undefined } + } + if (!(topInstance as any).loaded) { + return { ...topInstance, component: Loading, aside: undefined } + } + return { + ...topInstance, + component: registered[topInstance.type!].component, + aside: registered[topInstance.type!].aside, + } + }) + export const history = ref>([]) + export const mounted = ref>({}) + + export function init() { + history.value = [] + mounted.value = {} + } + + export async function dispose() { + const topInstance = top() + const dispose = topInstance?.lifecycles?.onUnmount + dispose + ? dispose(topInstance).then(() => { + topInstance.loaded = false + }) + : Promise.resolve() + } + + export function register( + type: K, + component: Component, + aside?: Component, + handlers?: Lifecycle + ) { + registered[type] = { component, aside, handlers } + } + + export async function create( + type: K, + data: ToCreate, + handlers?: Lifecycle + ) { + const scene: Prototype = data + scene.type = type + scene.lifecycles = handlers || registered[scene.type]?.handlers || {} + + const uid = `#${scene.type}-${scene.uid}` + if (scene.uid && mounted.value[uid]) { + return uid + } + + const id = scene.uid ? uid : genWindowId() + scene.id = id + scene.type = type + scene.loaded = true + scene.pinned = false + + mounted.value[id] = scene + + if (scene.lifecycles?.onInit) { + scene.loaded = false + await scene.lifecycles.onInit(scene) + mounted.value[id].loaded = true + } + + return id + } + + export function rend(id: ID) { + const index = history.value.findIndex((value) => value === id) + const instance = mounted.value[id] + const topInstance = top() + + if (!instance || (index === history.value.length - 1 && history.value.length)) { + return + } + + const processUnmount = topInstance?.lifecycles?.onUnmount + ? ((topInstance.loaded = false), + topInstance?.lifecycles.onUnmount(topInstance).then(() => { + topInstance.loaded = true + })) + : Promise.resolve() + + if (index > 0) { + history.value.splice(index, 1) + } + + history.value.push(id) + + instance.loaded = false + instance.lifecycles?.onMount + ? processUnmount.then(() => { + return instance.lifecycles?.onMount!(instance).then(() => { + instance.loaded = true + }) + }) + : processUnmount.then(() => { + instance.loaded = true + }) + } + + export function close(id: ID) { + const index = history.value.findIndex((value) => value === id) + if (index !== -1) { + history.value.splice(index, 1) + } + delete mounted.value[id] + } + + export function pop() { + const id = history.value.pop()! + delete mounted.value[id] + } + + function top(): Prototype | undefined { + const top = mounted.value[history.value[history.value.length - 1]] + return top || undefined + } + + function genWindowId(): ID { + startId += 1 // HACK + return startId.toString() + } +} + +export default Scene diff --git a/packages/im/client/app/scene/loading.vue b/packages/im/client/app/scene/loading.vue new file mode 100644 index 0000000..26c846e --- /dev/null +++ b/packages/im/client/app/scene/loading.vue @@ -0,0 +1,3 @@ + diff --git a/packages/im/client/app/scene/msgbox.vue b/packages/im/client/app/scene/msgbox.vue new file mode 100644 index 0000000..a0e0fae --- /dev/null +++ b/packages/im/client/app/scene/msgbox.vue @@ -0,0 +1,84 @@ + + + From b69c61ff92c217589cfec83b17a93f9318052fa2 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:34:24 +0800 Subject: [PATCH 34/50] feat(webui): user module --- packages/im/client/app/user/index.ts | 21 +++ packages/im/client/app/user/login-form.vue | 191 +++++++++++++++++++++ packages/im/client/app/user/my-info.vue | 56 ++++++ packages/im/client/app/user/search.vue | 55 ++++++ 4 files changed, 323 insertions(+) create mode 100644 packages/im/client/app/user/index.ts create mode 100644 packages/im/client/app/user/login-form.vue create mode 100644 packages/im/client/app/user/my-info.vue create mode 100644 packages/im/client/app/user/search.vue diff --git a/packages/im/client/app/user/index.ts b/packages/im/client/app/user/index.ts new file mode 100644 index 0000000..2806e5b --- /dev/null +++ b/packages/im/client/app/user/index.ts @@ -0,0 +1,21 @@ +import { Context } from '@cordisjs/client' +import type { Im } from '@satorijs/plugin-im' +import Login from './login-form.vue' + +export { default as MyInfoScene } from './my-info.vue' +export { default as GlobalSearchScene } from './search.vue' + +export function getDisplayName(user: Im.User, friend?: { name: string }, member?: Im.Member) { + let expr = user.nick || user.name + if (member) { + expr = member.nick || member.name || expr + } + if (friend) { + expr = friend.name || expr + } + return expr! +} + +export default function (ctx: Context) { + ctx.app.component('im-login', Login) +} diff --git a/packages/im/client/app/user/login-form.vue b/packages/im/client/app/user/login-form.vue new file mode 100644 index 0000000..20b1399 --- /dev/null +++ b/packages/im/client/app/user/login-form.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/packages/im/client/app/user/my-info.vue b/packages/im/client/app/user/my-info.vue new file mode 100644 index 0000000..6bf8bc8 --- /dev/null +++ b/packages/im/client/app/user/my-info.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/im/client/app/user/search.vue b/packages/im/client/app/user/search.vue new file mode 100644 index 0000000..274da64 --- /dev/null +++ b/packages/im/client/app/user/search.vue @@ -0,0 +1,55 @@ + + + From 32269bce2ca75f2bc54578f934120f769ce9bff6 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:34:51 +0800 Subject: [PATCH 35/50] feat(webui): add im components --- packages/im/client/components/button.vue | 30 ++++ packages/im/client/components/divider.vue | 71 +++++++++ packages/im/client/components/essential.vue | 41 ++++++ packages/im/client/components/form/form.vue | 16 +++ packages/im/client/components/form/item.vue | 26 ++++ packages/im/client/components/fs/file.vue | 68 +++++++++ packages/im/client/components/fs/index.ts | 1 + .../im/client/components/grouper/default.vue | 28 ++++ .../im/client/components/grouper/index.ts | 52 +++++++ packages/im/client/components/more.vue | 34 +++++ packages/im/client/components/role/avatar.vue | 89 +++++++++--- packages/im/client/components/role/index.ts | 12 +- .../im/client/components/role/role-tag.vue | 35 ++++- .../im/client/components/role/selector.vue | 136 ++++++++++++++++++ .../im/client/components/role/user-card.vue | 1 + .../im/client/components/search/global.vue | 108 ++++++++++++++ .../im/client/components/stepper/index.ts | 10 ++ packages/im/client/components/stepper/step.ts | 49 +++++++ .../im/client/components/stepper/step.vue | 31 ++++ .../im/client/components/stepper/stepper.vue | 34 +++++ packages/im/client/components/tab/index.ts | 2 +- packages/im/client/components/tab/tab.vue | 73 +++++++--- 22 files changed, 897 insertions(+), 50 deletions(-) create mode 100644 packages/im/client/components/button.vue create mode 100644 packages/im/client/components/divider.vue create mode 100644 packages/im/client/components/essential.vue create mode 100644 packages/im/client/components/form/form.vue create mode 100644 packages/im/client/components/form/item.vue create mode 100644 packages/im/client/components/fs/file.vue create mode 100644 packages/im/client/components/fs/index.ts create mode 100644 packages/im/client/components/grouper/default.vue create mode 100644 packages/im/client/components/grouper/index.ts create mode 100644 packages/im/client/components/more.vue create mode 100644 packages/im/client/components/role/selector.vue create mode 100644 packages/im/client/components/role/user-card.vue create mode 100644 packages/im/client/components/search/global.vue create mode 100644 packages/im/client/components/stepper/index.ts create mode 100644 packages/im/client/components/stepper/step.ts create mode 100644 packages/im/client/components/stepper/step.vue create mode 100644 packages/im/client/components/stepper/stepper.vue diff --git a/packages/im/client/components/button.vue b/packages/im/client/components/button.vue new file mode 100644 index 0000000..ad1f27c --- /dev/null +++ b/packages/im/client/components/button.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/im/client/components/divider.vue b/packages/im/client/components/divider.vue new file mode 100644 index 0000000..323256c --- /dev/null +++ b/packages/im/client/components/divider.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/im/client/components/essential.vue b/packages/im/client/components/essential.vue new file mode 100644 index 0000000..3345960 --- /dev/null +++ b/packages/im/client/components/essential.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/im/client/components/form/form.vue b/packages/im/client/components/form/form.vue new file mode 100644 index 0000000..2d5e258 --- /dev/null +++ b/packages/im/client/components/form/form.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/im/client/components/form/item.vue b/packages/im/client/components/form/item.vue new file mode 100644 index 0000000..8027865 --- /dev/null +++ b/packages/im/client/components/form/item.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/im/client/components/fs/file.vue b/packages/im/client/components/fs/file.vue new file mode 100644 index 0000000..be0ce1c --- /dev/null +++ b/packages/im/client/components/fs/file.vue @@ -0,0 +1,68 @@ + + + diff --git a/packages/im/client/components/fs/index.ts b/packages/im/client/components/fs/index.ts new file mode 100644 index 0000000..1c5c97c --- /dev/null +++ b/packages/im/client/components/fs/index.ts @@ -0,0 +1 @@ +export { default as FilePicker } from './file.vue' diff --git a/packages/im/client/components/grouper/default.vue b/packages/im/client/components/grouper/default.vue new file mode 100644 index 0000000..cef9994 --- /dev/null +++ b/packages/im/client/components/grouper/default.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/im/client/components/grouper/index.ts b/packages/im/client/components/grouper/index.ts new file mode 100644 index 0000000..99091a5 --- /dev/null +++ b/packages/im/client/components/grouper/index.ts @@ -0,0 +1,52 @@ +export interface Resolver { + readonly name: string + readonly filter?: (self: Resolver, data: T) => boolean + readonly options?: { + unique?: boolean + byName?: boolean + } +} + +export interface Group { + name: string + data: T[] +} + +export function filterByGroupAttr(self: Resolver, data: T) { + return data.group === self.name +} + +class Grouper { + constructor(private _data: T[], private _groupers: Resolver[]) {} + + createGroup(...groups: Resolver[]) { + this._groupers.push(...groups) + } + + resolve(): Group[] { + const result = [] as Group[] + let data = [...this._data] + + for (let i = 0; i < this._groupers.length; i++) { + const grouper = this._groupers[i] + let grouped: Group = { + name: this._groupers[i].name, + data: [], + } + + for (let j = 0; j < this._data.length; j++) { + if (grouper.filter && grouper.filter(grouper, data[j])) { + grouped.data.push(data[j]) + grouper.options?.unique && data.slice(j, 1) + } + } + + if (grouped.data.length > 0 && grouped.name) { + result.push(grouped) + } + } + return result + } +} + +export default Grouper diff --git a/packages/im/client/components/more.vue b/packages/im/client/components/more.vue new file mode 100644 index 0000000..f8119b8 --- /dev/null +++ b/packages/im/client/components/more.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/im/client/components/role/avatar.vue b/packages/im/client/components/role/avatar.vue index 8fe9ada..a7a9579 100644 --- a/packages/im/client/components/role/avatar.vue +++ b/packages/im/client/components/role/avatar.vue @@ -1,40 +1,51 @@ + + diff --git a/packages/im/client/components/role/index.ts b/packages/im/client/components/role/index.ts index f9ee8f6..dbae146 100644 --- a/packages/im/client/components/role/index.ts +++ b/packages/im/client/components/role/index.ts @@ -1,10 +1,10 @@ -import { App } from 'vue' +import { Context } from '@cordisjs/client' import Avatar from './avatar.vue' -import Login from './login-form.vue' import Tag from './role-tag.vue' +import UserSelector from './selector.vue' -export default function (app: App) { - app.component('k-im-avatar', Avatar) - app.component('k-im-login', Login) - app.component('k-im-tag', Tag) +export default function (ctx: Context) { + ctx.app.component('im-avatar', Avatar) + ctx.app.component('im-tag', Tag) + ctx.app.component('user-selector', UserSelector) } diff --git a/packages/im/client/components/role/role-tag.vue b/packages/im/client/components/role/role-tag.vue index 650edb1..98fad47 100644 --- a/packages/im/client/components/role/role-tag.vue +++ b/packages/im/client/components/role/role-tag.vue @@ -1,9 +1,38 @@ + + diff --git a/packages/im/client/components/role/selector.vue b/packages/im/client/components/role/selector.vue new file mode 100644 index 0000000..639f21b --- /dev/null +++ b/packages/im/client/components/role/selector.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/im/client/components/role/user-card.vue b/packages/im/client/components/role/user-card.vue new file mode 100644 index 0000000..cc340bc --- /dev/null +++ b/packages/im/client/components/role/user-card.vue @@ -0,0 +1 @@ + diff --git a/packages/im/client/components/search/global.vue b/packages/im/client/components/search/global.vue new file mode 100644 index 0000000..54afae0 --- /dev/null +++ b/packages/im/client/components/search/global.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/packages/im/client/components/stepper/index.ts b/packages/im/client/components/stepper/index.ts new file mode 100644 index 0000000..464dd5e --- /dev/null +++ b/packages/im/client/components/stepper/index.ts @@ -0,0 +1,10 @@ +import { App } from 'vue' +import Stepper from './stepper.vue' +import Step from './step.vue' + +export { Stepper } from './step' + +export default function (app: App) { + app.component('im-stepper', Stepper) + app.component('step-item', Step) +} diff --git a/packages/im/client/components/stepper/step.ts b/packages/im/client/components/stepper/step.ts new file mode 100644 index 0000000..38b2c81 --- /dev/null +++ b/packages/im/client/components/stepper/step.ts @@ -0,0 +1,49 @@ +export type StepCallback = (data: any) => any + +export namespace Step { + export type Status = 'inactive' | 'active' | 'completed' +} + +export class Stepper { + data: any = {} + steps: Array = [] + index: number = 0 + + addStep(callback: (data: any) => any) { + this.steps.push(callback) + } + + submit() { + this.steps[this.steps.length - 1](this.data) + } + + next() { + if (this.index < this.steps.length - 1) { + if (this.index) { + this.data = this._execute() + } + this.index++ + } + } + + prev() { + if (this.index > 0) { + this.index-- + } + } + + hasNext() { + return this.index < this.steps.length - 1 + } + + hasPrev() { + return this.index > 0 + } + + private _execute() { + if (this.steps[this.index]) { + return this.steps[this.index](this.data) + } + return this.data + } +} diff --git a/packages/im/client/components/stepper/step.vue b/packages/im/client/components/stepper/step.vue new file mode 100644 index 0000000..4444f18 --- /dev/null +++ b/packages/im/client/components/stepper/step.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/im/client/components/stepper/stepper.vue b/packages/im/client/components/stepper/stepper.vue new file mode 100644 index 0000000..1104cd1 --- /dev/null +++ b/packages/im/client/components/stepper/stepper.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/im/client/components/tab/index.ts b/packages/im/client/components/tab/index.ts index a548377..e1184f7 100644 --- a/packages/im/client/components/tab/index.ts +++ b/packages/im/client/components/tab/index.ts @@ -2,5 +2,5 @@ import { App } from 'vue' import Tab from './tab.vue' export default function (app: App) { - app.component('k-im-tab', Tab) + app.component('im-tab', Tab) } diff --git a/packages/im/client/components/tab/tab.vue b/packages/im/client/components/tab/tab.vue index 4c65b88..473a751 100644 --- a/packages/im/client/components/tab/tab.vue +++ b/packages/im/client/components/tab/tab.vue @@ -1,52 +1,85 @@ From bc0f11a3ad19c3894d7fd3f4c1f41bdcbdbbb15b Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:35:06 +0800 Subject: [PATCH 36/50] feat(utils): im utilities --- packages/im/client/utils/index.ts | 46 +++++++++++++++++++++++++++++++ packages/im/client/utils/time.ts | 16 +++++++++++ packages/utils/src/index.ts | 26 +++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 packages/im/client/utils/index.ts create mode 100644 packages/im/client/utils/time.ts create mode 100644 packages/utils/src/index.ts diff --git a/packages/im/client/utils/index.ts b/packages/im/client/utils/index.ts new file mode 100644 index 0000000..b473dce --- /dev/null +++ b/packages/im/client/utils/index.ts @@ -0,0 +1,46 @@ +export * from './time' + +export function lockdown any>( + fn: T +): (...args: Parameters) => void { + let lock: boolean + + return async function (...args: Parameters) { + if (!lock) { + lock = true + try { + await fn.apply(this, args) + } finally { + lock = false + } + } + } +} + +export function throttle any>( + fn: T, + duration: number = 1000 +): (...args: Parameters) => void { + let inThrottle: boolean + + return function (...args: Parameters) { + if (!inThrottle) { + inThrottle = true + fn.apply(this, args) + setTimeout(() => (inThrottle = false), duration) + } + } +} + +export function debounce any>( + fn: T, + duration: number = 1000 +): (...args: Parameters) => void { + let timeout: ReturnType | null = null + + return function (...args: Parameters) { + clearTimeout(timeout!) + + timeout = setTimeout(() => fn.apply(this, args), duration) + } +} diff --git a/packages/im/client/utils/time.ts b/packages/im/client/utils/time.ts new file mode 100644 index 0000000..52a1362 --- /dev/null +++ b/packages/im/client/utils/time.ts @@ -0,0 +1,16 @@ +export function formatTimestamp(ts: number, format: string = 'ymdhms'): string { + const date = new Date(ts) + const lowerFormat = format.toLowerCase() + + const options: Intl.DateTimeFormatOptions = { + year: format.includes('y') ? 'numeric' : undefined, + month: format.includes('m') ? 'long' : undefined, + day: format.includes('d') ? 'numeric' : undefined, + hour: format.includes('h') ? '2-digit' : undefined, + minute: format.includes('m') ? '2-digit' : undefined, + second: format.includes('s') ? '2-digit' : undefined, + } + + const formatter = new Intl.DateTimeFormat('zh-CN', options) + return formatter.format(date) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000..3f6a330 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,26 @@ +import { Schema } from 'cordis' + +export { v4 as genId } from 'uuid' + +const _patterns = { + name: Schema.string().pattern(/^[a-zA-Z0-9_]{1,16}$/), + nick: Schema.string().pattern(/^[^<>?/*'"\\]{1,16}$/), + password: Schema.string().pattern(/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[\W_]).{8,16}$/), +} + +export function validate(type: keyof typeof _patterns, value: string): boolean { + try { + _patterns[type](value) + } catch (e) { + return false + } + return true +} + +export function v1Wrapper( + callback: (...args: any[]) => Promise +): (data: T) => Promise { + return async function (data: T) { + return callback(...Object.values(data)) + } +} From a586738058eda983ef75462064f55018bf8bf6b1 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 2 Sep 2024 16:35:33 +0800 Subject: [PATCH 37/50] feat(webui): add im client service --- packages/im/client/index.ts | 325 +++++++++++++++++++++++++++++++++--- 1 file changed, 300 insertions(+), 25 deletions(-) diff --git a/packages/im/client/index.ts b/packages/im/client/index.ts index 5c4f59f..ad72758 100644 --- a/packages/im/client/index.ts +++ b/packages/im/client/index.ts @@ -1,37 +1,312 @@ -import { reactive } from 'vue' -import { Context, Dict } from '@cordisjs/client' -import HTTP from '@cordisjs/plugin-http' -import Satori, { Bot, Universal } from '@satorijs/core' +import { computed, reactive, ref, watch } from 'vue' +import { Context, send, Service } from '@cordisjs/client' +import {} from '@cordisjs/loader' +import { Dict } from '@satorijs/core' +import {} from '@satorijs/plugin-im-webui' +import type { Im } from '@satorijs/plugin-im' +import shared from './shared' +import components from './components' +import webui, { Scene, ImApp } from './app' +import { getDisplayName } from './app/user' import 'virtual:uno.css' -import install from './components' -import IMClient from './index.vue' +export { default as shared } from './shared' +export * from './icons' +export * from '@satorijs/plugin-im-utils' declare module '@cordisjs/client' { - interface Context {} + interface Context { + 'im.client': ChatService + } } -interface CachedLogin { - user: Bot - login: Universal.Login | null +interface Cache { + users: Dict + members: Dict> + tasks: Dict> } -interface CachedData { - guilds: Universal.Guild[] - friends: Universal.User[] +interface LoggedUser { + login: Im.Login + friends: Array + guilds: Array + notifications: Array + messages: Dict<{ + data: Array + unread: number + }> } -export default (ctx: Context) => { - let loginUser = reactive>({}) - ctx.plugin(HTTP) - ctx.plugin(Satori) - - ctx.app.use(install) - ctx.page({ - name: 'IM-test', - path: '/im', - icon: 'activity:chat', - component: IMClient, - }) +const localToken = shared.value.token +let chat = reactive({ + users: {}, + members: {}, + tasks: {}, + messages: {}, +} as any) + +export default class ChatService extends Service { + static inject = { optional: ['im.client'] } + + status = ref<'verifying' | 'logging-in' | 'syncing' | 'logged'>('verifying') + + login = computed(() => chat.login) + + guilds = computed(() => chat.guilds) + + friends = computed(() => chat.friends) + + get _cache() { + return chat + } + + constructor(public ctx: Context) { + super(ctx, 'im.client', true) + ctx.plugin(components) + ctx.plugin(webui) + + if (localToken) { + send('im/v1/login-add', { login: { token: localToken } as Im.Login }) + .then((data) => { + this.setLogin(data) + }) + .catch(() => { + shared.value.token = '' + this.status.value = 'logging-in' + }) + } else { + this.status.value = 'logging-in' + } + + ctx.addEventListener('beforeunload', async () => { + await Scene.dispose() + }) + + ctx.page({ + name: 'IM', + path: '/im', + icon: 'activity:chat', + component: ImApp, + }) + } + + // TODO: promisified. + async setLogin(login: Im.Login) { + chat.login = null as any + chat.friends = null as any + chat.guilds = null as any + chat.notifications = null as any + chat.messages = {} as any + + chat.login = login + chat.login.selfId = login.user!.id + chat.users[login.selfId!] = login.user! + shared.value.token = login.token + + this.status.value = 'syncing' + + try { + Scene.init() // trans to Service? + + chat.guilds = await send('im/v1/guild/fetch-all', { login: chat.login }) + chat.friends = await send('im/v1/friend/fetch-all', { login: chat.login }) + + const channels = await send('im/v1/channel/recent', { login: chat.login }) + + for (const channel of channels) { + // TODO: Dont import value like Im.Channel.Type + if (channel.type !== 1) { + const guild = chat.guilds.find((value) => value.id === channel.guild?.id)! + await Scene.create('chat-guild', { + uid: channel.id, + title: guild.name!, + subtitle: channel.name, + channel: channel, + guild, + }) + continue + } + const friend = chat.friends.find((value) => value.id === channel.friend?.id)! + await Scene.create('chat-friend', { + uid: channel.id, + title: getDisplayName(friend.user, { name: friend.nick! }), + subtitle: channel!.name, + friend, + channel, + }) + } + + for (const friend of chat.friends) { + chat.users[friend.user.id] = friend.user + } + } catch (err) { + this.status.value = 'logging-in' + return + } + + this.status.value = 'logged' + } + + getLogin() { + return chat.login + } + + logout() { + chat.login = undefined! + shared.value.token = '' + + this.status.value = 'logging-in' + } + + async getUser(uid: string) { + if (!chat.users[uid] && !chat.tasks[uid]) { + chat.users[uid] = await send('im/v1/user/fetch', { login: chat.login, uid }) + delete chat.tasks[uid] + } + return chat.users[uid] + } + + async getMember(gid: string, uid: string) { + const key = `${gid}-${uid}` + if (!chat.members[gid]) chat.members[gid] = {} + if (!chat.members[gid][uid] && !chat.tasks[key]) { + chat.members[gid][uid] = await send('im/v1/guild-member/fetch', { + login: chat.login, + gid, + uid, + }) + delete chat.tasks[key] + } + return chat.members[gid][uid] + } + + async _getAllMembers(gid: string) { + const result = await send('im/v1/guild-member/fetch-all', { + login: chat.login, + gid, + }) + + for (let i = 0; i < result.length; i++) { + chat.members[gid][result[i].user.id] = result[i] + } + + return Object.values(chat.members[gid]) + } + + mountMessage(message: Im.Message) { + chat.messages[message.channel!.id].data.push(message) + } + + async getMessageData(channel: Im.Channel) { + const cid = channel.id + const messages = chat.messages[cid] + + if (!messages) { + const _ = undefined + chat.messages[cid] = { + data: [], + unread: await send('im/v1/message/unread-count', { login: chat.login, cid }), + } + await this.updateMessageList(channel, _, _, 30) + } + + return chat.messages[cid] + } + + async updateMessageList( + channel: Im.Channel, + dir: 'before' | 'after' = 'before', + endpoint: number = new Date().getTime(), + limit: number = 10 + ) { + const cid = channel.id + const data = await send('im/v1/message/fetch', { + login: chat.login, + cid, + endpoint, + dir, + limit, + }) + + for (const message of data) { + const user = await this.getUser(message.user!.id) + message.user = user + if (channel.type !== 1) { + const member = await this.getMember(channel.guild!.id, message.user!.id) + message.member = member + } + } + + dir === 'before' + ? chat.messages[cid].data.unshift(...data.reverse()) + : chat.messages[cid].data.push(...data) + } + + _eventHandler = async (event: Im.Event) => { + this.ctx.logger('im.client').info(`[event] type: ${event.type}`) + + const item = event.type.split('-')[0] + const ptr = chat[`${item}s`] + + switch (true) { + case 'message' === event.type: { + const response = event.message! + const cid = response.channel!.id + + if (!chat.messages[cid]) { + if (response.member) { + const guild = this.guilds.value.find((value) => value.id === response.member!.guild.id)! + const channel = guild?.channels?.find((value) => value.id === cid)! + await Scene.create('chat-guild', { + uid: cid, + title: guild?.name!, + subtitle: channel!.name, + guild, + channel, + }) + } else { + const friend = this.friends.value.find((value) => value.channel?.id === cid)! + const channel = friend.channel! + await Scene.create('chat-friend', { + uid: cid, + title: getDisplayName(friend.user, { name: friend.nick! }), + subtitle: channel!.name, + friend, + channel, + }) + } + } + + if (response.user!.id === chat.login.selfId) { + chat.messages[cid].data.some( + (value, i) => value.sid === response.sid && chat.messages[cid].data.splice(i, 1) + ) + } else { + chat.messages[cid].unread += 1 + } + + const { sid: _, ...message } = response + if (chat.messages[message.channel!.id]) { + chat.messages[message.channel!.id].data.push(message) + } + break + } + case /-added$/.test(event.type): { + ptr.push(event[item]) + break + } + case /-deleted$/.test(event.type): { + ptr.filter((value: any) => value.id !== event[item]) + break + } + case /-updated$/.test(event.type): { + const existingItem = ptr.find((value: any) => value.id === event[item]) + existingItem ? Object.assign(existingItem, event[item]) : ptr.push(event[item]) + break + } + default: + console.error(`cannot resolve event: ${event}`) + } + } } From 3ef43d5a8b69034566d5c5fc0d860a3969ae21da Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Wed, 4 Sep 2024 01:39:29 +0800 Subject: [PATCH 38/50] fix(webui): fix watcher logic --- packages/im/client/app/index.vue | 4 ++-- packages/im/client/components/role/selector.vue | 4 ++-- packages/im/client/components/search/global.vue | 13 +++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/im/client/app/index.vue b/packages/im/client/app/index.vue index 75d7e7d..7b22a8c 100644 --- a/packages/im/client/app/index.vue +++ b/packages/im/client/app/index.vue @@ -67,9 +67,9 @@ const rpcData = useRpc() watch( () => rpcData.value.eventChan, - () => { + (value) => { if (chat.status.value === 'logged') { - chat._eventHandler(rpcData.value.eventChan) + chat._eventHandler(value) } } ) diff --git a/packages/im/client/components/role/selector.vue b/packages/im/client/components/role/selector.vue index 639f21b..77840f2 100644 --- a/packages/im/client/components/role/selector.vue +++ b/packages/im/client/components/role/selector.vue @@ -83,8 +83,8 @@ const leftValue = ref>([]) const rightValue = computed(() => leftValue.value.filter((item) => item.active)) watch( () => rightValue.value, - () => { - selected.value = rightValue.value.map((item) => item.user.id) + (right) => { + selected.value = right.map((item) => item.user.id) } ) diff --git a/packages/im/client/components/search/global.vue b/packages/im/client/components/search/global.vue index 54afae0..5848260 100644 --- a/packages/im/client/components/search/global.vue +++ b/packages/im/client/components/search/global.vue @@ -43,12 +43,13 @@ const except = computed(() => watch( () => props.keyword, - debounce(() => { - fetchPage(props.type as any, props.keyword).then((data) => { - page.data = data! - loaded.value = true - }) - }, 500) + (keyword) => + debounce(() => { + fetchPage(props.type as any, keyword).then((data) => { + page.data = data! + loaded.value = true + }) + }, 500) ) function isSelected(item: Im.User) { From f153122a24e86112d4e321b54e3248dcbf37b32a Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 01:22:00 +0800 Subject: [PATCH 39/50] feat(core): asset apis which include avatar & message.files --- packages/core/src/data.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/data.ts b/packages/core/src/data.ts index faf89e3..cda220f 100644 --- a/packages/core/src/data.ts +++ b/packages/core/src/data.ts @@ -7,9 +7,11 @@ import * as Model from './models' import { Login } from './types' import { genId } from '@satorijs/plugin-im-utils' +// TODO: Implement role permissions. class ImDataService extends Service { static inject = ['server', 'model', 'database'] // FIXME: self injection + bot: Model.BotData user: Model.UserData friend: Model.FriendData guild: Model.GuildData @@ -21,6 +23,7 @@ class ImDataService extends Service { super(ctx, 'im.data', true) // TODO: need better injections. + this.bot = new Model.BotData(this.ctx) this.user = new Model.UserData(this.ctx) this.friend = new Model.FriendData(this.ctx) this.guild = new Model.GuildData(this.ctx) @@ -74,20 +77,20 @@ class ImDataService extends Service { return urlPath } - // TODO: writeFile = async (login: Login, b64: string) => { const mType = b64.match(/^data:(.*?);base64,/) if (!mType) throw Error() const ext = mime.extension(mType[1]) - const filePath = path.join(this.config.assetPath, 'messages') + const filePath = path.join('messages', `${genId()}.${ext}`) + const filePathAbsolute = path.join(this.config.assetPath, filePath) + const fileData = b64.split('base64,')[1] - await fs.mkdir(filePath, { recursive: true }) - await fs.writeFile( - filePath + `/${genId()}-${login.selfId}.tmp.${ext}`, - Buffer.from(b64, 'base64') - ) + await fs.mkdir(path.dirname(filePathAbsolute), { recursive: true }) + await fs.writeFile(filePathAbsolute, Buffer.from(fileData, 'base64')) - return `${this.ctx.server.selfUrl}/${filePath}` + const urlPath = encodeURI(filePath.replace(/\\/g, '/')) + + return urlPath } } From e57f20a96267664c0d4061dff6bc2f83067e2e21 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 01:23:15 +0800 Subject: [PATCH 40/50] feat(bot): bot implementation --- packages/bot/package.json | 45 ++++++++ packages/bot/src/bot.ts | 217 +++++++++++++++++++++++++++++++++++++ packages/bot/src/index.ts | 4 + packages/bot/tsconfig.json | 10 ++ 4 files changed, 276 insertions(+) create mode 100644 packages/bot/package.json create mode 100644 packages/bot/src/bot.ts create mode 100644 packages/bot/src/index.ts create mode 100644 packages/bot/tsconfig.json diff --git a/packages/bot/package.json b/packages/bot/package.json new file mode 100644 index 0000000..5538820 --- /dev/null +++ b/packages/bot/package.json @@ -0,0 +1,45 @@ +{ + "name": "@satorijs/adapter-im", + "description": "Adapter of Satori IM", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/im" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "chat", + "im", + "adapter" + ], + "peerDependencies": { + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.2.6", + "@satorijs/plugin-im": "^0.0.0", + "@satorijs/plugin-server": "^2.7.1" + }, + "devDependencies": { + "@cordisjs/client": "^0.1.12", + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.2.6" + }, + "dependencies": { + "cordis": "^3.18.0" + } +} diff --git a/packages/bot/src/bot.ts b/packages/bot/src/bot.ts new file mode 100644 index 0000000..8710d12 --- /dev/null +++ b/packages/bot/src/bot.ts @@ -0,0 +1,217 @@ +import { Bot, Context, Fragment, h, Schema, Universal } from '@satorijs/core' +import { SendOptions } from '@satorijs/protocol' +import { Im } from '@satorijs/plugin-im' + +// TODO: implement ImMessageDecoder. +export class ImBot extends Bot { + static inject = ['im.data', 'im.auth', 'im'] + + private login: Im.Login | null = null + + async getUser(userId: string) { + return this.ctx['im.data'].user.fetch(this.login!, userId) + } + + async getLogin(): Promise { + if (this.config.type === 'token') { + this.login = await this.ctx['im.auth'].authenticateWithToken({ + token: this.config.token, + } as any) + } else { + this.login = await this.ctx['im.auth'].authenticate( + this.config.name, + this.config.password, + this.selfId + ) + } + + this.user = this.login?.user! + + return this.login! + } + + async dispose() { + await this.ctx['im.auth'].logout(this.login) + } + + async getGuild(guildId: string) { + const data = await this.ctx['im.data'].guild.fetch(this.login!, guildId) + return data + } + + async getGuildList() { + const data = await this.ctx['im.data'].guild.list(this.login!) + return { + data, + } + } + + async getChannel(channelId: string) { + return this.ctx['im.data'].channel.fetch(this.login!, channelId) + } + + async getGuildMember(guildId: string, userId: string) { + return this.ctx['im.data'].guild.Member.fetch(this.login!, guildId, userId) + } + + async getGuildMemberList(guildId: string) { + const data = await this.ctx['im.data'].guild.Member.list(this.login!, guildId) + return { + data, + } + } + + async createChannel( + guildId: string, + data: Partial + ): Promise { + if (!data.name) throw new Error('name is required.') + return this.ctx['im.data'].channel.create(this.login!, guildId, data.name) + } + + async updateChannel(guildId: string, data: Partial) { + await this.ctx['im.data'].channel.update(this.login!, guildId, data.name) + } + + async deleteChannel(channelId: string) { + await this.ctx['im.data'].channel.softDel(this.login!, channelId) + } + + async muteChannel(channelId: string) { + await this.ctx['im.data'].channel.updateSettings(this.login, channelId, { + level: Im.NotifyLevels.BLOCKED, + }) + } + + async handleFriendRequest(messageId: string, approve: boolean, comment?: string) { + await this.ctx['im.data'].notification.reply(this.login!, messageId, approve) + } + + async handleGuildMemberRequest(messageId: string, approve: boolean, comment?: string) { + await this.ctx['im.data'].notification.reply(this.login!, messageId, approve) + } + + async kickGuildMember(guildId: string, userId: string) { + await this.ctx['im.data'].guild.Member.kick(this.login!, guildId, userId) + } + + // TODO: + async getGuildRoleList(guildId: string, next?: string) { + const data = await this.ctx['im.data'].guild.Role.list(this.login!, guildId) + + return { + data, + } + } + + async createGuildRole(guildId: string, data: Partial) { + if (!data.name || !data.color) throw new Error('') + const res = await this.ctx['im.data'].guild.Role.create( + this.login!, + guildId, + data.name!, + data.color! + ) + return res + } + + async updateGuildRole(guildId: string, roleId: string, data: Partial) {} + + async deleteGuildRole(guildId: string, roleId: string) { + await this.ctx['im.data'].guild.Role.hardDel(this.login!, guildId, roleId) + } + + setGuildMemberRole(guildId: string, userId: string, roleId: string) { + return this.ctx['im.data'].guild.Role.set(this.login!, roleId, userId) + } + + unsetGuildMemberRole(guildId: string, userId: string, roleId: string) { + return this.ctx['im.data'].guild.Role.unset(this.login!, roleId, userId) + } + + // TODO: + async getMessageList( + channelId: string, + messageId?: string, + direction: Universal.Direction = 'before', + limit?: number, + order: Universal.Order = 'asc' + ) { + const messages = await this.ctx['im.data'].message.fetch(this.login!, channelId) + return messages + } + + // TODO: + async sendMessage( + channelId: string, + content: h.Fragment, + guildId?: string, + options?: SendOptions + ) { + let guild: Im.Guild | undefined = guildId ? { id: guildId } : undefined + await this.ctx['im.data'].message.create(this.login!, { + content: content as string, + guild, + channel: { id: channelId } as Im.Channel, + }) + return [] + } + + // HACK: No use of guildId. + async sendPrivateMessage( + userId: string, + content: h.Fragment, + guildId?: string, + options?: SendOptions + ) { + const channel = (await this.ctx['im.data'].friend.fetch(this.login!, userId))?.channel + if (!channel) { + throw new Error('no permission to send private message to selected user.') + } + await this.ctx['im.data'].message.create(this.login!, { + content: content as string, + channel: { id: channel.id } as Im.Channel, + }) + + return [] + } + + // TODO: how we resolve the satori element to im message? + async editMessage(channelId: string, messageId: string, content: Fragment) { + await this.ctx['im.data'].message.update(this.login!, channelId, messageId, content as string) + } + + async deleteMessage(channelId: string, messageId: string) { + await this.ctx['im.data'].message.softDel(this.login!, channelId, messageId) + } + + async updateCommands(commands: Universal.Command[]): Promise { + await this.ctx['im.data'].bot._updateCommands(commands) + } +} + +export namespace ImBot { + export interface Config { + type: 'token' | 'password' + name: string + password: string + token: string + } + + export const Config: Schema = Schema.intersect([ + Schema.object({ + type: Schema.union(['token', 'password']).default('password').description('登录方式'), + }), + Schema.union([ + Schema.object({ + type: Schema.const('token').required(), + token: Schema.string().description('Bot 用户令牌。').role('secret').required(), + }), + Schema.object({ + type: Schema.const('password'), + name: Schema.string().description('Bot 用户名称').required(), + password: Schema.string().description('Bot 登录所用的密码').role('secret').required(), + }), + ]), + ]) as any +} diff --git a/packages/bot/src/index.ts b/packages/bot/src/index.ts new file mode 100644 index 0000000..00421a1 --- /dev/null +++ b/packages/bot/src/index.ts @@ -0,0 +1,4 @@ +import {} from '@satorijs/plugin-im' +import { ImBot } from './bot' + +export default ImBot diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json new file mode 100644 index 0000000..fd164e6 --- /dev/null +++ b/packages/bot/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + }, + "include": [ + "src" + ], +} \ No newline at end of file From 2e01753ceef3bab6fefeb5bcd6a5bde0b293d534 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 01:24:24 +0800 Subject: [PATCH 41/50] feat(bot): supports of bot data --- packages/core/src/database.ts | 23 +++++++++++++++++++++++ packages/core/src/models/bot.ts | 23 +++++++++++++++++++++++ packages/core/src/types.ts | 9 +++++++++ 3 files changed, 55 insertions(+) create mode 100644 packages/core/src/models/bot.ts diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 2cbd977..84fb3c6 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -13,6 +13,10 @@ export default class ImDatabase { }, nick: 'char(255)', avatar: 'char(255)', + isBot: { + type: 'boolean', + initial: false, + }, }, { primary: ['id'], @@ -70,6 +74,25 @@ export default class ImDatabase { primary: ['user'], } ) + ctx.model.extend( + 'satori-im.bot.command', + { + bot: { + type: 'manyToOne', + table: 'satori-im.user', + target: 'commands', + }, + 'bot.id': 'char(255)', + 'parent.name': 'char(255)', + name: 'char(255)', + description: 'json', + arguments: 'json', + options: 'json', + }, + { + primary: ['bot.id', 'name', 'parent.name'], + } + ) // FIXME: combined unique doesnt support relation. ctx.model.extend( 'satori-im.friend', diff --git a/packages/core/src/models/bot.ts b/packages/core/src/models/bot.ts new file mode 100644 index 0000000..6eaa74b --- /dev/null +++ b/packages/core/src/models/bot.ts @@ -0,0 +1,23 @@ +import { createHash } from 'crypto' +import { Context } from '@satorijs/core' +import { Bot, Login } from '../types' + +export class BotData { + constructor(public ctx: Context) {} + + _createBot = async (name: string, password: string) => { + const crypted = createHash('sha256').update(password).digest('hex') + await this.ctx.database.create('satori-im.user.auth', { + user: { id: 'koishi', name, isBot: true }, + password: crypted, + }) + } + + fetchCommands = async (login: Login, id: string): Promise> => { + return this.ctx.database.get('satori-im.bot.command', { bot: { id } }) + } + + _updateCommands = async (commands: Bot.Command[]) => { + await this.ctx.database.upsert('satori-im.bot.command', commands) + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5f6f279..4a04230 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3,6 +3,7 @@ import { Row } from 'minato' declare module 'minato' { interface Tables { + 'satori-im.bot.command': Bot.Command 'satori-im.channel.settings': Channel.Settings 'satori-im.channel': Channel 'satori-im.friend.settings': Friend.Settings @@ -83,6 +84,7 @@ export interface User extends Universal.User { roles?: Array members?: Array deleted?: boolean + commands?: Array } export namespace User { @@ -104,6 +106,13 @@ export namespace User { } } +export namespace Bot { + export interface Command extends Universal.Command { + bot: User + parent: Command + } +} + export interface Friend { id: string self: User From cb2eaca3a79014c8a39c442568e355ab84f749dd Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 01:32:09 +0800 Subject: [PATCH 42/50] chore: tweak --- packages/bot/package.json | 86 +++++++-------- packages/core/package.json | 10 +- packages/core/src/auth.ts | 2 +- packages/core/src/index.ts | 2 + packages/core/src/models/guild.ts | 35 ++++-- packages/core/src/models/index.ts | 1 + packages/core/src/models/message.ts | 16 +-- packages/core/src/notifier.ts | 14 +-- packages/im/client/app/index.ts | 10 +- packages/im/client/app/index.vue | 2 +- .../im/client/app/navigation/friend-item.vue | 2 +- .../im/client/app/navigation/guild-item.vue | 2 +- packages/im/client/app/navigation/list.vue | 25 +++-- .../im/client/app/navigation/session-item.vue | 10 +- packages/im/package.json | 102 +++++++++--------- packages/im/src/index.ts | 23 +++- packages/utils/package.json | 2 +- 17 files changed, 187 insertions(+), 157 deletions(-) diff --git a/packages/bot/package.json b/packages/bot/package.json index 5538820..a764997 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -1,45 +1,45 @@ { - "name": "@satorijs/adapter-im", - "description": "Adapter of Satori IM", - "version": "0.0.0", - "type": "module", - "main": "lib/index.js", - "files": [ - "lib", - "dist" - ], - "author": "Shigma ", - "license": "MIT", - "scripts": { - "lint": "eslint src --ext .ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/koishijs/koishi-plugin-im.git", - "directory": "packages/im" - }, - "bugs": { - "url": "https://github.com/koishijs/koishi-plugin-im/issues" - }, - "keywords": [ - "satori", - "plugin", - "chat", - "im", - "adapter" - ], - "peerDependencies": { - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.2.6", - "@satorijs/plugin-im": "^0.0.0", - "@satorijs/plugin-server": "^2.7.1" - }, - "devDependencies": { - "@cordisjs/client": "^0.1.12", - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.2.6" - }, - "dependencies": { - "cordis": "^3.18.0" - } + "name": "@satorijs/adapter-im", + "description": "Bot of Satori IM", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/bot" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "chat", + "im", + "adapter" + ], + "peerDependencies": { + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.2.6", + "@satorijs/plugin-im": "^0.0.0", + "@satorijs/plugin-server": "^2.7.1" + }, + "devDependencies": { + "@cordisjs/client": "^0.1.12", + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.2.6" + }, + "dependencies": { + "cordis": "^3.18.0" + } } diff --git a/packages/core/package.json b/packages/core/package.json index 97db1ef..a4302e2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,7 +16,7 @@ "repository": { "type": "git", "url": "git+https://github.com/koishijs/koishi-plugin-im.git", - "directory": "packages/im" + "directory": "packages/core" }, "bugs": { "url": "https://github.com/koishijs/koishi-plugin-im/issues" @@ -39,18 +39,20 @@ } }, "peerDependencies": { - "@satorijs/core": "^4.2.6" + "@satorijs/core": "^4.2.6", + "@satorijs/plugin-im-utils": "^0.0.0" }, "devDependencies": { + "@cordisjs/client": "^0.1.12", "@cordisjs/plugin-server": "^0.2.3", "@satorijs/core": "^4.2.6", "@types/mime-types": "^2" }, "dependencies": { "@minatojs/driver-mysql": "^3.5.0", - "cordis": "^3.18.0", + "cordis": "^3.18.1", "cosmokit": "^1.6.2", "mime-types": "^2.1.35", - "minato": "^3.5.1" + "minato": "^3.5.0" } } diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 1f2f761..68e1125 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -15,7 +15,7 @@ export class ImAuthService extends Service { ctx.on('dispose', this._save) } - authenticate = async (name: string, password: string, clientId): Promise => { + authenticate = async (name: string, password: string, clientId: string): Promise => { const result = await this.ctx.database .select( 'satori-im.user.auth', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7f4ef49..524503b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,6 +3,7 @@ import ImDatabase from './database' import { ImAuthService } from './auth' import ImDataService from './data' import { ImEventService } from './notifier' +import { Event } from './types' export * as Im from './types' @@ -18,6 +19,7 @@ declare module 'cordis' { declare module '@satorijs/core' { interface Events { 'exit'(signal: NodeJS.Signals): Promise + 'im-message'(event: Event): Promise } } diff --git a/packages/core/src/models/guild.ts b/packages/core/src/models/guild.ts index ab32ff1..44dc5f2 100644 --- a/packages/core/src/models/guild.ts +++ b/packages/core/src/models/guild.ts @@ -1,6 +1,6 @@ import { $ } from 'minato' import { Context } from '@satorijs/core' -import { Channel, Guild, Login, Member, Role, ChunkOptions, extractSettings } from '../types' +import { Channel, Guild, Login, Member, Role, ChunkOptions } from '../types' import { genId } from '@satorijs/plugin-im-utils' export class GuildData { @@ -421,18 +421,11 @@ export class GuildData { return result }, - create: async ( - login: Login, - gid: string, - uid: string, - name: string, - color: number - ): Promise => { + create: async (login: Login, gid: string, name: string, color: number): Promise => { const result = await this.ctx.database.create('satori-im.role', { id: genId(), guild: { id: gid }, gid, - users: [{ id: uid }], name, color, }) @@ -453,6 +446,30 @@ export class GuildData { } }, + set: async (login: Login, rid: string, uid: string) => { + await this.ctx.database.set( + 'satori-im.role', + { + id: rid, + }, + { + users: { $upsert: { id: uid } }, + } + ) + }, + + unset: async (login: Login, rid: string, uid: string) => { + await this.ctx.database.set( + 'satori-im.role', + { + id: rid, + }, + { + users: { $remove: { id: uid } }, + } + ) + }, + hardDel: async (login: Login, rid: string) => { const result = await this.ctx.database.remove('satori-im.role', rid) if (!result.matched) { diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index e621847..2417af7 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -1,3 +1,4 @@ +export * from './bot' export * from './channel' export * from './friend' export * from './user' diff --git a/packages/core/src/models/message.ts b/packages/core/src/models/message.ts index 6b69c38..a187e31 100644 --- a/packages/core/src/models/message.ts +++ b/packages/core/src/models/message.ts @@ -110,7 +110,7 @@ export class MessageData { } update = async (login: Login, cid: string, mid: string, content: string) => { - const result = await this.ctx.database.set( + await this.ctx.database.set( 'satori-im.message.test', { id: mid, @@ -118,12 +118,6 @@ export class MessageData { }, { content } ) - if (!result.matched) { - throw new Error() - } - if (!result.modified) { - throw new Error() - } this.ctx.im.event.pushEvent({ selfId: login.selfId!, type: 'message-updated', @@ -133,13 +127,7 @@ export class MessageData { } softDel = async (login: Login, cid: string, mid: string) => { - const result = await this.ctx.database.set('satori-im.message.test', mid, { deleted: true }) - if (!result.matched) { - throw new Error() - } - if (!result.modified) { - throw new Error() - } + await this.ctx.database.set('satori-im.message.test', { id: mid }, { deleted: true }) this.ctx.im.event.pushEvent({ selfId: login.selfId!, type: 'message-deleted', diff --git a/packages/core/src/notifier.ts b/packages/core/src/notifier.ts index 6da7e24..013bc6a 100644 --- a/packages/core/src/notifier.ts +++ b/packages/core/src/notifier.ts @@ -1,12 +1,5 @@ -import { Context, Dict, Service, Universal } from '@satorijs/core' -import { Channel, Event, Friend, Guild, Login, Message, Notification, User } from './types' -import { genId, v1Wrapper } from '@satorijs/plugin-im-utils' - -declare module '@satorijs/core' { - interface Events { - 'im/subscribe'(data: { login: Login }): Promise - } -} +import { Context, Dict, Service } from '@satorijs/core' +import { Event, Login } from './types' export class ImEventService extends Service { static inject = ['database', 'im', 'im.auth'] @@ -30,6 +23,9 @@ export class ImEventService extends Service { } private _pushEvent(data: Event) { + // HACK: directly emit events to the only bot. + this.ctx.emit('im-message', data) + const receivers: Dict = {} if (data.guild) { for (let i = 0; i < data.guild.members!.length; i++) { diff --git a/packages/im/client/app/index.ts b/packages/im/client/app/index.ts index 0fbf032..5dd056c 100644 --- a/packages/im/client/app/index.ts +++ b/packages/im/client/app/index.ts @@ -60,9 +60,6 @@ export default function (ctx: Context) { set: (value) => { scene.friend.pinned = value // TODO: - // send('im/v1/friend/update-settings', { - - // }).catch(() => (scene.friend.pinned = !value)) }, }) @@ -74,6 +71,7 @@ export default function (ctx: Context) { '', set: (value) => (manualBrief.value = value), }) + scene.avatar = scene.friend.user.avatar }, onMount: async (scene) => { scene.unread = -1 // HACK: cannot set unread to 0 twice. @@ -104,11 +102,7 @@ export default function (ctx: Context) { if (scene.guild.settings?.[0].pinned) { scene.guild.settings[0].pinned = value } - // TODO: - // send('im/v1/friend/update-settings', { - - // }).catch(() => (scene.friend.pinned = !value)) }, }) @@ -120,6 +114,8 @@ export default function (ctx: Context) { '', set: (value) => (manualBrief.value = value), }) + + scene.avatar = scene.guild.avatar }, onMount: async (scene) => { scene.unread = -1 // HACK: cannot set unread to 0 twice. diff --git a/packages/im/client/app/index.vue b/packages/im/client/app/index.vue index 7b22a8c..ddf3f22 100644 --- a/packages/im/client/app/index.vue +++ b/packages/im/client/app/index.vue @@ -4,7 +4,7 @@ - + 同步数据中... diff --git a/packages/im/client/app/navigation/friend-item.vue b/packages/im/client/app/navigation/friend-item.vue index e8b448f..4e745a2 100644 --- a/packages/im/client/app/navigation/friend-item.vue +++ b/packages/im/client/app/navigation/friend-item.vue @@ -3,7 +3,7 @@ class="friend-item flex flex-row items-center gap-3 h-7 p-3 cursor-pointer" @click="handleSelect" > - + diff --git a/packages/im/client/app/navigation/guild-item.vue b/packages/im/client/app/navigation/guild-item.vue index 352b194..bb64c79 100644 --- a/packages/im/client/app/navigation/guild-item.vue +++ b/packages/im/client/app/navigation/guild-item.vue @@ -4,7 +4,7 @@ :class="{ active: !isFolded }" @click="handleSwitch()" > - + diff --git a/packages/im/client/app/navigation/list.vue b/packages/im/client/app/navigation/list.vue index 1d3b979..ce23245 100644 --- a/packages/im/client/app/navigation/list.vue +++ b/packages/im/client/app/navigation/list.vue @@ -1,12 +1,10 @@ -
-
-
- -
-
- -
-
-
- -
-
-
- - - -
-
- - -
-
+
+
- - diff --git a/packages/im/client/app/messenger/editor/echolink.ts b/packages/im/client/app/messenger/editor/echolink.ts new file mode 100644 index 0000000..196b646 --- /dev/null +++ b/packages/im/client/app/messenger/editor/echolink.ts @@ -0,0 +1,152 @@ +import { Dict } from '@cordisjs/client' + +type KeyCallback = (event: KeyboardEvent) => void +type InputCallback = (event: InputEvent) => void + +interface Combo { + ctrl: boolean + alt: boolean + key: string +} + +interface Hold { + duration: number + key: string +} + +// TODO: +class EchoLink { + private keyDownCallbacks: Dict = {} + private keyUpCallbacks: Dict = {} + private keyHoldCallbacks: Dict = {} + private keyComboCallbacks: Dict = {} + private beforeInputCallback: Dict = {} + private inputCallback: InputCallback = () => {} + private ordinaryCallbacks: Dict = {} + + private _isMonitoring = false + private _noSpread = false + private _noDefault = false + + get input() { + return this._handleInput + } + + get keydown() { + return this._handleKeydown + } + + get keyup() { + return this._handleKeyup + } + + constructor(private target: HTMLElement) {} + + start(name?: string) { + if (name === 'beforeinput') { + this.target.addEventListener('beforeinput', this._beforeInput) + return + } + + if (!this._isMonitoring) { + this.target.addEventListener('keydown', this._handleKeydown) + this.target.addEventListener('beforeinput', this._beforeInput) + this.target.addEventListener('input', this._handleInput as any) + this.target.addEventListener('keyup', this._handleKeyup) + this._isMonitoring = true + console.log('EchoLink activated.') + } + } + + stop(name: string, type?: string) { + if (name === 'beforeinput') { + this.target.removeEventListener('beforeinput', this._beforeInput) + return + } + + this.target.removeEventListener('keydown', this._handleKeydown) + this.target.removeEventListener('beforeinput', this._beforeInput) + this.target.removeEventListener('input', this._handleInput as any) + this.target.removeEventListener('keyup', this._handleKeyup) + this._isMonitoring = false + console.log('EchoLink deactivated.') + } + + getKey(key: string) { + return { + down: this.keyDownCallbacks[key], + up: this.keyUpCallbacks[key], + hold: this.keyHoldCallbacks[key], + } + } + + on(name: string, callback: (...args: any[]) => any) { + this.ordinaryCallbacks[name] = callback + } + + onBeforeInput(type: string, callback: InputCallback) { + this.beforeInputCallback[type] = callback + } + + onInput(callback: InputCallback) { + this.inputCallback = callback + } + + onKeyDown(key: string, callback: KeyCallback) { + this.keyDownCallbacks[key] = callback + } + + onKeyUp(key: string, callback: KeyCallback) { + this.keyUpCallbacks[key] = callback + } + + onKeyHold(key: string, callback: KeyCallback) { + this.keyHoldCallbacks[key] = callback + } + + onCombo(keys: string[], callback: KeyCallback) { + const comboKey = this.generateComboKey(keys) + this.keyComboCallbacks[comboKey] = callback + } + + generateComboKey(keys: string[]): string { + return keys.sort().join('+') + } + + _handleKeydown = (event: KeyboardEvent) => { + const key = event.key + const callback = this.keyDownCallbacks[key] + if (callback) { + callback(event) + } + } + + _handleKeyup = (event: KeyboardEvent) => { + const key = event.key + const callback = this.keyUpCallbacks[key] + if (callback) { + callback(event) + } + } + + _handleInput = (event: InputEvent) => { + if (this._noSpread) { + event.stopPropagation() + } + if (this._noDefault) { + event.preventDefault() + } + + this.inputCallback(event) + } + + _beforeInput = (event: InputEvent) => { + const callback = this.beforeInputCallback[event.inputType] || this.beforeInputCallback['*'] + + if (callback) { + callback(event) + } + } +} + +export default EchoLink diff --git a/packages/im/client/app/messenger/editor/editor.ts b/packages/im/client/app/messenger/editor/editor.ts new file mode 100644 index 0000000..0379f8f --- /dev/null +++ b/packages/im/client/app/messenger/editor/editor.ts @@ -0,0 +1,533 @@ +import { ref, VNode, nextTick, h } from 'vue' +import { debounce } from '../../../utils' +import EchoLink from './echolink' +import TextResolver from './resolver' + +type Caret = { + line: Range + text: Range + _node?: Range // HACK: _node will not update like Caret.line & Caret.text + _offset?: Range +} +type Range = { start: T; end: T } + +function initCaret(): Caret { + return { + line: { + start: 0, + end: 0, + }, + text: { + start: 0, + end: 0, + }, + } +} + +function isBlockElement(node: Node): boolean { + return node instanceof HTMLElement && node.classList.contains('markdown-block') +} + +function isInlineElement(node: Node): boolean { + return node instanceof HTMLElement && node.classList.contains('markdown-inline') +} + +function _isInline(inline: Node, node: Node): boolean { + return inline.contains(node) || inline === node +} + +function _isInBlock(block: Node, node: Node): boolean { + return block.contains(node) || block === node +} + +function _isBlockAtLastLine(root: Node, node: Node): boolean { + return node.parentElement === root && (root.lastChild?.contains(node) || root.lastChild === node) +} + +export class Editor { + keyListener: EchoLink + mdResolver: TextResolver + + _raws: string[] = [''] + _caret: Caret = initCaret() + cursorOffset: number = 0 + + composition = { + composing: false, + text: '', + } + + result = ref([]) + + get raw(): string { + return this._raws.join('\n') + } + + get caret(): Caret { + this.secureCaret() + return this._caret + } + + get _lineElements(): Node[] { + return Array.from(this.ref.childNodes).filter((node) => node.nodeName === 'DIV') + } + + constructor(public ref: HTMLElement) { + this.keyListener = new EchoLink(ref) + this.mdResolver = new TextResolver(true) + + document.addEventListener( + 'selectionchange', + debounce(() => { + const selection = window.getSelection()! + + if ( + this.ref.contains(selection.getRangeAt(0).commonAncestorContainer) && + !this.composition.composing + ) { + this._caret = this.getCaret(selection)! + } + }, 100) + ) + + this.ref.addEventListener('paste', (event) => { + const text = event.clipboardData?.getData('text/plain') || '' + + const pos = this._caret + if (pos.line.start !== pos.line.end || pos.text.start !== pos.text.end) { + this.deleteInRaw() + } + + this.insertInRaw(text) + this._updateText() + event.preventDefault() + }) + + this.ref.addEventListener('cut', (event) => { + const selected = window.getSelection()?.toString()! + event.clipboardData?.setData('text/plain', selected) + this.deleteInRaw() + this._updateText() + event.preventDefault() + }) + + this.ref.addEventListener('compositionstart', async (event) => { + this.composition.composing = true + + event.preventDefault() + }) + + this.ref.addEventListener('compositionupdate', (event) => { + this.composition.text = event.data + }) + + this.ref.addEventListener('compositionend', (event) => { + this.insertInRaw(this.composition.text || '') + + this.composition.composing = false + this.composition.text = '' + + this._updateText() + }) + + this.keyListener.onBeforeInput('insertCompositionText', (event) => {}) + + this.keyListener.onBeforeInput('deleteContentBackward', (event) => { + this.deleteInRaw() + this._updateText() + event.preventDefault() + }) + + this.keyListener.onBeforeInput('insertParagraph', (event) => { + this.insertInRaw('\n') + this._updateText() + event.preventDefault() + }) + + this.keyListener.onBeforeInput('*', (event) => { + const pos = this._caret + + if (pos.line.start !== pos.line.end || pos.text.start !== pos.text.end) { + this.deleteInRaw() + } + + if (!this.composition.composing) { + this.insertInRaw(event.data || '') + } + + this._updateText() + + event.preventDefault() + }) + + this.keyListener.onInput((event) => { + console.warn(event) + + this.resetCaret() + }) + + this.keyListener.onKeyDown('Tab', (event) => { + this.insertInRaw(' ') + this._updateText() + event.preventDefault() + }) + + this.keyListener.start() + + this._updateText().then(() => { + this.resetCaret() + }) + } + + editorWalker = (root: Node) => { + let inlineStack: Node[] = [] + + return document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, + (node) => { + const currentInline = inlineStack[inlineStack.length - 1] + let flag: number = NodeFilter.FILTER_SKIP + if (node.nodeType === Node.ELEMENT_NODE) { + if (isInlineElement(node)) { + inlineStack.push(node) + } + + flag = NodeFilter.FILTER_ACCEPT + } else if (node.nodeType === Node.TEXT_NODE) { + if (currentInline && _isInline(currentInline, node)) { + flag = NodeFilter.FILTER_ACCEPT + } else if (!this.composition.composing && node.textContent) { + console.warn('illegal element detected') + node.textContent = '' + flag = NodeFilter.FILTER_REJECT + } + + if (currentInline && currentInline.lastChild === node) { + console.log(node) + inlineStack.splice(-1) + } + } + + return flag + } + ) + } + + // This operation will trigger asynchronous Vue DOM updates. After calling this method, + // nextTick() must be called to ensure that the DOM has been updated before performing + async _updateText() { + this.result.value = this.mdResolver.resolveContent(this.raw) + await nextTick() + } + + resetContent() { + this._raws = [''] + + this._updateText() + this._caret = initCaret() + } + + getCaret(selection: Selection): Caret | null { + if (!selection) return null + + function reorderSelection(selection: Selection) { + const anchorNode = selection.anchorNode + const anchorOffset = selection.anchorOffset + const focusNode = selection.focusNode + const focusOffset = selection.focusOffset + + const isAnchorBeforeFocus = + anchorNode === focusNode + ? anchorOffset < focusOffset + : (document.compareDocumentPosition(anchorNode!) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 + + if (!isAnchorBeforeFocus) { + return { + startNode: focusNode, + startOffset: focusOffset, + endNode: anchorNode, + endOffset: anchorOffset, + } + } + + return { + startNode: anchorNode, + startOffset: anchorOffset, + endNode: focusNode, + endOffset: focusOffset, + } + } + + const reordered = reorderSelection(selection) + + const result: Caret = { + line: { + start: this._lineElements.length - 1, + end: this._lineElements.length - 1, + }, + text: { + start: 0, + end: 0, + }, + _node: { + start: reordered.startNode!, + end: reordered.endNode!, + }, + _offset: { + start: reordered.startOffset, + end: reordered.endOffset, + }, + } + + const walker = this.editorWalker(this.ref) + let current: Node | null = null + let lastBlock: Node | null = null + let count = 0 + + let flag = [false, false] + while ((current = walker.nextNode()) && !flag.every(Boolean)) { + if ( + lastBlock && + !_isInBlock(lastBlock, current) && + !_isBlockAtLastLine(this.ref, lastBlock) + ) { + count += 1 + lastBlock = null + } + + if (isBlockElement(current)) { + lastBlock = current + } + + if (current.nodeType === Node.TEXT_NODE) { + if (current === result._node!.start) { + result.text.start = count + result._offset!.start + flag[0] = true + } + if (current === result._node!.end) { + result.text.end = count + result._offset!.end + flag[1] = true + } + + count += current.textContent?.length || 0 + } + } + + result.line.start = 0 + result.line.end = 0 + + console.log(`Recorded caret. At: ${JSON.stringify(result)}`) + + return result + } + + resetCaret() { + this.setCaret(this._caret.line, this._caret.text) + } + + setCaret(line: Range, offset: Range) { + this.secureCaret(line, offset) + + const range = document.createRange() + const selection = window.getSelection() + if (!selection) return + + const absolute = (line: number, offset: number): number => + this._raws.slice(0, line).reduce((sum, str) => sum + str.length, 0) + offset + + const start = this._walkLine(this.ref, absolute(line.start, offset.start)) + const end = + line.start === line.end && offset.start === offset.end + ? start + : this._walkLine(this.ref, absolute(line.end, offset.end)) + + range.setStart(start.node, start.offset) + range.setEnd(end.node, end.offset) + + selection.removeAllRanges() + selection.addRange(range) + + console.log(`Caret settled. line: ${line.start}, offset: ${offset.start}`) + } + + deleteInRaw() { + const lineStart = this.caret.line.start + const lineEnd = this.caret.line.end + const offsetStart = this.caret.text.start + const offsetEnd = this.caret.text.end + const rawArray = this._raws + + // multiple selection + if (offsetStart !== offsetEnd || lineStart !== lineEnd) { + if (lineStart !== lineEnd) { + rawArray[lineStart] = rawArray[lineStart].slice(0, offsetStart) + rawArray[lineEnd] = rawArray[lineEnd].slice(offsetEnd) + rawArray.splice(lineStart + 1, lineEnd - lineStart - 1) + } else { + rawArray[lineStart] = + rawArray[lineStart].slice(0, offsetStart) + rawArray[lineStart].slice(offsetEnd) + } + + this.caret.text.start = this.caret.text.end = offsetStart + this.caret.line.start = this.caret.line.end = lineStart + return + } + + // single selection + if (offsetStart <= 0) { + if (lineStart === 0) return + + const previous = lineStart - 1 + this.caret.text.start = this.caret.text.end = rawArray[previous].length + rawArray[previous] += rawArray[lineStart] + rawArray.splice(lineStart, 1) + this.caret.line.start = this.caret.line.end = previous + } else { + if (offsetStart === rawArray[lineStart].length) { + rawArray[lineStart] = rawArray[lineStart].slice(0, offsetStart - 1) + } else { + rawArray[lineStart] = + rawArray[lineStart].slice(0, offsetStart - 1) + rawArray[lineStart].slice(offsetStart) + } + this.caret.text.end = this.caret.text.start -= 1 + } + } + + insertInRaw(text: string) { + const index = this.caret.line.start + const offset = this.caret.text.start + const rawArray = this._raws + + const beforeText = rawArray[index].slice(0, offset) + const afterText = rawArray[index].slice(offset) + + rawArray[index] = beforeText + text + afterText + + console.log(this._raws) + + this.moveCaret({ x: text.length }) + } + + // This method could parse abstractive text line to concrete location of node. + // values must be ensured to be safe, or call secureCaret() before the method. + _walkLine(line: number, offset: number): any + _walkLine(root: Node, offset: number): any + _walkLine(lineOrNode: number | Node, offset: number) { + const root = typeof lineOrNode === 'number' ? this._lineElements[lineOrNode] : lineOrNode + const walker = this.editorWalker(root) + + let count = 0 + let current: Node | null = null + let lastBlock: Node | null = null + while ((current = walker.nextNode())) { + if ( + lastBlock && + !_isInBlock(lastBlock, current) && + !_isBlockAtLastLine(this.ref, lastBlock) + ) { + count += 1 + lastBlock = null + } + + if (isBlockElement(current)) { + lastBlock = current + } + + if (current.nodeType === Node.TEXT_NODE) { + const length = current.textContent?.length || 0 + + if (offset <= length + count) { + break + } + + count += length + } + } + + if (!current) { + current = root + // while (root.lastChild) { + // current = root.lastChild + // } + } + + return { + node: current, + offset: Math.min(offset - count, current.textContent?.length || Number.MAX_SAFE_INTEGER), + } + } + + moveCaret(offset: { y?: number; x?: number }) { + let caret = this._caret + let y = caret.line.start + (offset.y || 0) + let x = caret.text.start + (offset.x || 0) + + const yLength = this._raws.length + if (y < 0) { + y = 0 + x = 0 + } else if (y >= yLength) { + y = yLength - 1 + x = this._raws[y].length + } + + let xLength = this._raws[y].length + + while (offset.x && (x < 0 || x > xLength)) { + const direction = Math.sign(x) + + y += direction + if (y < 0) { + y = 0 + x = 0 + break + } else if (y >= yLength) { + y = yLength - 1 + x = xLength + break + } + + const next = this._raws[y].length + + if (x < 0) { + x -= (next + 1) * direction + } else { + x -= (xLength + 1) * direction + } + + xLength = next + } + + caret.line.start = y + caret.line.end = caret.line.start + caret.text.start = x + caret.text.end = caret.text.start + console.log(`Caret moved. y: ${offset.y}, x: ${offset.x}`) + } + + secureCaret(): void + secureCaret(y: Range, x: Range): void + secureCaret(y?: Range, x?: Range) { + const yLength = this._raws.length - 1 + + y || (y = this._caret.line) + x || (x = this._caret.text) + + y.start = secure(y.start, yLength) + y.end = secure(y.end, yLength) + + const xStartLength = this._raws[y.start].length + const xEndLength = this._raws[y.end].length + x.start = secure(x.start, xStartLength) + x.end = secure(x.end, xEndLength) + + function secure(index: number, length: number): number { + if (index >= 0 || index <= length) { + return index + } + console.warn('out of range') + return index < 0 ? 0 : length + } + } +} diff --git a/packages/im/client/app/messenger/editor/editor.vue b/packages/im/client/app/messenger/editor/editor.vue new file mode 100644 index 0000000..efad77c --- /dev/null +++ b/packages/im/client/app/messenger/editor/editor.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/packages/im/client/app/messenger/editor/index.ts b/packages/im/client/app/messenger/editor/index.ts new file mode 100644 index 0000000..dfb898b --- /dev/null +++ b/packages/im/client/app/messenger/editor/index.ts @@ -0,0 +1,2 @@ +export * from './editor' +export { default as ImEditor } from './editor.vue' diff --git a/packages/im/client/app/messenger/editor/md-rules.ts b/packages/im/client/app/messenger/editor/md-rules.ts new file mode 100644 index 0000000..7b2d559 --- /dev/null +++ b/packages/im/client/app/messenger/editor/md-rules.ts @@ -0,0 +1,71 @@ +import { h, VNode, resolveComponent } from 'vue' +import { + Lexer, + marked, + Tokens, + TokenizerExtensionFunction, + Tokenizer, + TokenizerObject, +} from 'marked' + +declare module 'marked' { + namespace Tokens { + interface Component { + type: 'component' + raw: string + text: string + } + + interface At { + type: 'at' + raw: string + name: string + } + } +} + +export class ImTokenizer extends Tokenizer { + constructor() { + super() + } +} + +export const componentRule: TokenizerExtensionFunction = function ( + this, + src +): Tokens.Component | undefined { + const match = src.match(/\/(\{[^}]*\});/) + if (match) { + return { + type: 'component', + raw: match[0], + text: match[1], + } as any + } +} + +export const atRule: TokenizerExtensionFunction = function (this, src): Tokens.At | undefined { + const match = src.match(/^@([^\s\n]*)/) + + if (match) { + return { + type: 'at', + raw: match[0], + name: match[1], + } + } +} + +/* + FIXME: The ReturnType here is expected to ReturnType, + but should actually be number. +*/ +// @ts-ignore +export const atFinder: TokenizerExtensionFunction = function (this, src) { + const mentionPattern = /@([a-zA-Z0-9_]+)/g + let match = mentionPattern.exec(src) + if (match) { + return match.index + } + return Infinity +} diff --git a/packages/im/client/app/messenger/editor/resolver.ts b/packages/im/client/app/messenger/editor/resolver.ts new file mode 100644 index 0000000..89fff81 --- /dev/null +++ b/packages/im/client/app/messenger/editor/resolver.ts @@ -0,0 +1,140 @@ +import { h, VNode, resolveComponent } from 'vue' +import { + Lexer, + marked, + Token, + TokenizerExtensionFunction, + Tokens, + walkTokens, + TokensList, +} from 'marked' +import { Element } from '@satorijs/core' +import { + MdCodeBlock, + MdCodeSpan, + MdEmphasis, + MdList, + MdSpace, + MdParagraph, + MdHeading, + MdStrong, + MdText, + At, +} from '../message' +import { ImTokenizer, atRule, atFinder } from './md-rules' +import { genId } from '@satorijs/plugin-im-utils' + +interface Renderer { + default: (token: T) => VNode + editor: (token: T) => VNode + compliant: (token: T) => Element +} + +const renderers: Renderer[] = [] +const inlinePlugins: TokenizerPlugin[] = [] +const blockPlugins: TokenizerPlugin[] = [] + +interface TokenizerPlugin { + rule: TokenizerExtensionFunction + locator: TokenizerExtensionFunction | undefined +} + +class TextResolver { + _inlinePlugins: TokenizerExtensionFunction[] = [atRule] + _blockPlugins: TokenizerExtensionFunction[] = [] + + get _tokenizer() { + return (text: string): Token[] => { + return marked.lexer(text, { + tokenizer: new ImTokenizer(), + extensions: { + inline: this._inlinePlugins, + block: this._blockPlugins, + startInline: [atFinder], + } as any, + }) + } + } + + constructor(public editMode: boolean) {} + + withInlinePlugin(...plugins: TokenizerExtensionFunction[]) { + this._inlinePlugins.push(...plugins) + } + + withBlockPlugin(...plugins: TokenizerExtensionFunction[]) { + this._blockPlugins.push(...plugins) + } + + resolveLine(content: string): VNode { + console.log(this._tokenizer(content)) + return h('div', this._resolveChildren(this._tokenizer(content))) + } + + resolveContent(content: string): VNode[] { + console.log(this._tokenizer(content)) + console.log(this._resolveChildren(this._tokenizer(content))) + return this._resolveChildren(this._tokenizer(content)) + } + + resolveToken(token: Token): VNode | null { + if (token.type === 'at') { + return h(At, { token }) + } else if (token.type === 'component') { + console.log(token) + const obj = JSON.parse(token.text) + const { name, ...args } = obj + if (!resolveComponent(name)) { + h('text', token.text) + } + return h(resolveComponent(name), { ...args }) + } else if (token.type === 'code') { + return h(MdCodeBlock, { token }) + } else if (token.type === 'codespan') { + return h(MdCodeSpan, { text: token.raw }) + } else if (token.type === 'paragraph') { + // HACK: Use genId() to force an update to resolve the IME problem. + return h(MdParagraph, { token, children: this._resolveChildren(token.tokens!) }) + } else if (token.type === 'image') { + return h(resolveComponent('md-image'), { src: token.href }) + } else if (token.type === 'blockquote') { + return h('blockquote', this._resolveChildren(token.tokens!)) // TODO + } else if (token.type === 'text') { + return h(MdText, { text: token.text }) + } else if (token.type === 'em') { + const children = this._resolveChildren(token.tokens!) + return h(MdEmphasis, { token, children }) + } else if (token.type === 'strong') { + const children = this._resolveChildren(token.tokens!) + return h(MdStrong, { token, children }) + } else if (token.type === 'del') { + return h('del', this._resolveChildren(token.tokens!)) + } else if (token.type === 'link') { + return h('a', { href: token.href }, this._resolveChildren(token.tokens!)) + } else if (token.type === 'space') { + return h(MdSpace, { text: token.raw }) + } else if (token.type === 'list') { + return h(MdList, { token }) + } else if (token.type === 'heading') { + return h(MdHeading, { token, children: this._resolveChildren(token.tokens!) }) + } + return h('text', token.raw) + } + + _resolveChildren(tokens: Token[]): VNode[] { + if (!tokens.length) { + return [] + } + return tokens.map((token) => this.resolveToken(token)).filter(Boolean) as VNode[] + } +} + +export function registerRenderer(renderer: Renderer) { + renderers.push(renderer) +} + +export function registerInlineTokenizer(tokenizer: TokenizerPlugin) {} + +export function registerBlockTokenizer(tokenizer: TokenizerPlugin) {} + +export default TextResolver diff --git a/packages/im/client/app/messenger/message/at.vue b/packages/im/client/app/messenger/message/at.vue new file mode 100644 index 0000000..1e6b970 --- /dev/null +++ b/packages/im/client/app/messenger/message/at.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/break.vue b/packages/im/client/app/messenger/message/break.vue new file mode 100644 index 0000000..ec65b9e --- /dev/null +++ b/packages/im/client/app/messenger/message/break.vue @@ -0,0 +1,5 @@ + diff --git a/packages/im/client/app/messenger/message/code-block.vue b/packages/im/client/app/messenger/message/code-block.vue new file mode 100644 index 0000000..791051e --- /dev/null +++ b/packages/im/client/app/messenger/message/code-block.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/code-span.vue b/packages/im/client/app/messenger/message/code-span.vue new file mode 100644 index 0000000..6c6c384 --- /dev/null +++ b/packages/im/client/app/messenger/message/code-span.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/emphasis.vue b/packages/im/client/app/messenger/message/emphasis.vue new file mode 100644 index 0000000..02259cb --- /dev/null +++ b/packages/im/client/app/messenger/message/emphasis.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/im/client/app/messenger/message/heading.vue b/packages/im/client/app/messenger/message/heading.vue new file mode 100644 index 0000000..98b126c --- /dev/null +++ b/packages/im/client/app/messenger/message/heading.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/im/client/app/messenger/message/list.vue b/packages/im/client/app/messenger/message/list.vue new file mode 100644 index 0000000..3d1c090 --- /dev/null +++ b/packages/im/client/app/messenger/message/list.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/im/client/app/messenger/message/md-block.vue b/packages/im/client/app/messenger/message/md-block.vue new file mode 100644 index 0000000..dc3e255 --- /dev/null +++ b/packages/im/client/app/messenger/message/md-block.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/im/client/app/messenger/message/md-inline.vue b/packages/im/client/app/messenger/message/md-inline.vue new file mode 100644 index 0000000..b0c5b85 --- /dev/null +++ b/packages/im/client/app/messenger/message/md-inline.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/im/client/app/messenger/message/paragraph.vue b/packages/im/client/app/messenger/message/paragraph.vue new file mode 100644 index 0000000..40f2261 --- /dev/null +++ b/packages/im/client/app/messenger/message/paragraph.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/im/client/app/messenger/message/space.vue b/packages/im/client/app/messenger/message/space.vue new file mode 100644 index 0000000..c4b9a66 --- /dev/null +++ b/packages/im/client/app/messenger/message/space.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/im/client/app/messenger/message/strong.vue b/packages/im/client/app/messenger/message/strong.vue new file mode 100644 index 0000000..b5e15a7 --- /dev/null +++ b/packages/im/client/app/messenger/message/strong.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/im/client/app/messenger/message/text.vue b/packages/im/client/app/messenger/message/text.vue new file mode 100644 index 0000000..e19dd60 --- /dev/null +++ b/packages/im/client/app/messenger/message/text.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/im/client/app/messenger/resolver/index.ts b/packages/im/client/app/messenger/resolver/index.ts new file mode 100644 index 0000000..c4b38ed --- /dev/null +++ b/packages/im/client/app/messenger/resolver/index.ts @@ -0,0 +1,24 @@ +import { h, VNode, resolveComponent } from 'vue' +import { + Lexer, + marked, + Token, + TokenizerExtensionFunction, + Tokens, + walkTokens, + TokensList, +} from 'marked' +import { Context } from '@satorijs/core' +import { + MdCodeBlock, + MdCodeSpan, + MdList, + MdSpace, + MdParagraph, + MdHeading, + MdText, + At, +} from '../message' +import { genId } from '@satorijs/plugin-im-utils' + +export default function (ctx: Context) {} diff --git a/packages/im/client/app/messenger/resolver/resolver.ts b/packages/im/client/app/messenger/resolver/resolver.ts new file mode 100644 index 0000000..377ceb5 --- /dev/null +++ b/packages/im/client/app/messenger/resolver/resolver.ts @@ -0,0 +1,152 @@ +import { h, VNode, resolveComponent } from 'vue' +import { + Lexer, + marked, + Token, + TokenizerExtensionFunction, + Tokens, + walkTokens, + TokensList, +} from 'marked' +import { Element } from '@satorijs/core' +import { + MdCodeBlock, + MdCodeSpan, + MdList, + MdSpace, + MdParagraph, + MdHeading, + MdText, + At, +} from '../message' +import { ImTokenizer, atRule } from './rules' +import { genId } from '@satorijs/plugin-im-utils' + +export type Renderer = (token: T) => R + +export interface RendererPlugin { + type: string + default: Renderer + editor: Renderer + satori: Renderer +} + +export interface TokenizerPlugin { + rule: TokenizerExtensionFunction + locator?: TokenizerExtensionFunction +} + +const renderers: RendererPlugin[] = [] +const inlinePlugins: TokenizerPlugin[] = [] +const blockPlugins: TokenizerPlugin[] = [] + +class TextResolver { + _inlinePlugins: TokenizerExtensionFunction[] = [] + _blockPlugins: TokenizerExtensionFunction[] = [] + + get _tokenizer() { + return (text: string): Token[] => { + if (!text) + return [ + { + type: 'paragraph', + raw: '', + text: '', + }, + ] + return marked.lexer(text, { + tokenizer: new ImTokenizer(), + extensions: { + inline: this._inlinePlugins, + block: this._blockPlugins, + startInline: [], + } as any, + }) + } + } + + constructor(public editMode: boolean) {} + + withInlinePlugin(...plugins: TokenizerExtensionFunction[]) { + this._inlinePlugins.push(...plugins) + } + + withBlockPlugin(...plugins: TokenizerExtensionFunction[]) { + this._blockPlugins.push(...plugins) + } + + resolveLine(content: string): VNode { + console.log(this._tokenizer(content)) + console.log(marked(content)) + return h('div', this._resolveChildren(this._tokenizer(content))) + } + + resolveContent(content: string): VNode[] { + console.log(this._tokenizer(content)) + console.log(marked(content)) + console.log(this._resolveChildren(this._tokenizer(content))) + return this._resolveChildren(this._tokenizer(content)) + } + + resolveToken(token: Token): VNode | null { + if (token.type === 'at') { + return h(At, { token }) + } else if (token.type === 'component') { + console.log(token) + const obj = JSON.parse(token.text) + const { name, ...args } = obj + if (!resolveComponent(name)) { + h('text', token.text) + } + return h(resolveComponent(name), { ...args }) + } else if (token.type === 'code') { + return h(MdCodeBlock, { token }) + } else if (token.type === 'codespan') { + return h(MdCodeSpan, { text: token.raw }) + } else if (token.type === 'paragraph') { + // HACK: Use genId() to force an update to resolve the IME problem. + return h(MdParagraph, { token, children: this._resolveChildren(token.tokens!) }) + } else if (token.type === 'image') { + return h(resolveComponent('md-image'), { src: token.href }) + } else if (token.type === 'blockquote') { + return h('blockquote', this._resolveChildren(token.tokens!)) // TODO + } else if (token.type === 'text') { + return h(MdText, { text: token.text }) + } else if (token.type === 'em') { + const children = this._resolveChildren(token.tokens!) + return h('em', this.editMode ? ['*', ...children, '*'] : children) + } else if (token.type === 'strong') { + const children = this._resolveChildren(token.tokens!) + return h('b', this.editMode ? ['**', ...children, '**'] : children) + } else if (token.type === 'del') { + return h('del', this._resolveChildren(token.tokens!)) + } else if (token.type === 'link') { + return h('a', { href: token.href }, this._resolveChildren(token.tokens!)) + } else if (token.type === 'space') { + return h(MdSpace, { text: token.raw }) + } else if (token.type === 'list') { + return h(MdList, { token }) + } else if (token.type === 'heading') { + return h(MdHeading, { token, children: this._resolveChildren(token.tokens!) }) + } + return h('text', token.raw) + } + + _resolveChildren(tokens: Token[]): VNode[] { + if (!tokens) { + console.log('no tokens') + return [] + } + return tokens.map((token) => this.resolveToken(token)).filter(Boolean) as VNode[] + } +} + +// export function registerRenderer(renderer: Renderer) { +// renderers.push(renderer) +// } + +export function registerInlineTokenizer(tokenizer: TokenizerPlugin) {} + +export function registerBlockTokenizer(tokenizer: TokenizerPlugin) {} + +export default TextResolver diff --git a/packages/im/client/app/messenger/resolver/rules.ts b/packages/im/client/app/messenger/resolver/rules.ts new file mode 100644 index 0000000..38f7978 --- /dev/null +++ b/packages/im/client/app/messenger/resolver/rules.ts @@ -0,0 +1,73 @@ +import { h, VNode, resolveComponent } from 'vue' +import { + Lexer, + marked, + Tokens, + TokenizerExtensionFunction, + Tokenizer, + TokenizerObject, +} from 'marked' +import { TokenizerPlugin, RendererPlugin } from './resolver' + +declare module 'marked' { + namespace Tokens { + interface Component { + type: 'component' + raw: string + text: string + } + + interface At { + type: 'at' + raw: string + name: string + } + } +} + +export class ImTokenizer extends Tokenizer { + constructor() { + super() + } +} + +export const componentRule: TokenizerPlugin = { + rule(this, src): Tokens.Component | undefined { + const match = src.match(/\/(\{[^}]*\});/) + if (match) { + return { + type: 'component', + raw: match[0], + text: match[1], + } as any + } + }, +} + +export const atRule: TokenizerPlugin = { + rule(this, src): Tokens.At | undefined { + const match = src.match(/^@([^\s\n]*)/) + + if (match) { + return { + type: 'at', + raw: match[0], + name: match[1], + } + } + }, + + /* + FIXME: The ReturnType here is expected to ReturnType, + but should actually be number. +*/ + // @ts-ignore + locator(this, src) { + const mentionPattern = /@([a-zA-Z0-9_]+)/g + let match = mentionPattern.exec(src) + if (match) { + return match.index + } + return Infinity + }, +} diff --git a/packages/im/client/app/messenger/resolver/templates.ts b/packages/im/client/app/messenger/resolver/templates.ts new file mode 100644 index 0000000..c5e6493 --- /dev/null +++ b/packages/im/client/app/messenger/resolver/templates.ts @@ -0,0 +1,27 @@ +import { h } from 'vue' +import { h as Element } from '@satorijs/core' +import { Tokens } from 'marked' +import { RendererPlugin } from './resolver' +import { + MdCodeBlock, + MdCodeSpan, + MdList, + MdSpace, + MdParagraph, + MdHeading, + MdText, + At, +} from '../message' + +export const headingRenderer: RendererPlugin = { + type: 'heading', + default(token) { + return h('heading') + }, + editor(token) { + return h(MdHeading, { token, children: this.recursive(token.tokens) }) + }, + satori(token) { + return Element('h') + }, +} diff --git a/packages/im/client/components/bot/list.vue b/packages/im/client/components/bot/list.vue new file mode 100644 index 0000000..0764089 --- /dev/null +++ b/packages/im/client/components/bot/list.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/im/client/components/editor/emoji.vue b/packages/im/client/components/editor/emoji.vue new file mode 100644 index 0000000..2aa0e04 --- /dev/null +++ b/packages/im/client/components/editor/emoji.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/im/client/components/editor/index.ts b/packages/im/client/components/editor/index.ts new file mode 100644 index 0000000..87bf45f --- /dev/null +++ b/packages/im/client/components/editor/index.ts @@ -0,0 +1 @@ +export { default as EmojiPanel } from './emoji.vue' diff --git a/packages/im/client/components/role/selector-tiny.vue b/packages/im/client/components/role/selector-tiny.vue new file mode 100644 index 0000000..bd260b4 --- /dev/null +++ b/packages/im/client/components/role/selector-tiny.vue @@ -0,0 +1,60 @@ + + + From 723d99bb3ece9706a71baab753fa1045e841dbf7 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 02:08:23 +0800 Subject: [PATCH 48/50] chore: remove markdown-it --- packages/im/package.json | 101 +++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/packages/im/package.json b/packages/im/package.json index 8a061d3..d216741 100644 --- a/packages/im/package.json +++ b/packages/im/package.json @@ -1,54 +1,51 @@ { - "name": "@satorijs/plugin-im-webui", - "description": "IM for Satori", - "version": "0.0.0", - "type": "module", - "main": "lib/index.js", - "files": [ - "lib", - "dist" - ], - "author": "Shigma ", - "license": "MIT", - "scripts": { - "lint": "eslint src --ext .ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/koishijs/koishi-plugin-im.git", - "directory": "packages/im" - }, - "bugs": { - "url": "https://github.com/koishijs/koishi-plugin-im/issues" - }, - "keywords": [ - "satori", - "plugin", - "webui", - "chat", - "im" - ], - "cordis": { - "service": {} - }, - "peerDependencies": { - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.1.1", - "@satorijs/plugin-im": "^0.0.0", - "@satorijs/plugin-im-utils": "^0.0.0" - }, - "devDependencies": { - "@cordisjs/client": "^0.1.12", - "@cordisjs/plugin-http": "^0.5.3", - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.1.1", - "@satorijs/plugin-server": "^2.6.5", - "@types/markdown-it": "^14" - }, - "dependencies": { - "cordis": "^3.17.7", - "cosmokit": "^1.6.2", - "markdown-it": "^14.1.0", - "markdown-it-highlightjs": "^4.1.0" - } + "name": "@satorijs/plugin-im-webui", + "description": "IM for Satori", + "version": "0.0.0", + "type": "module", + "main": "lib/index.js", + "files": [ + "lib", + "dist" + ], + "author": "Shigma ", + "license": "MIT", + "scripts": { + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi-plugin-im.git", + "directory": "packages/im" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi-plugin-im/issues" + }, + "keywords": [ + "satori", + "plugin", + "webui", + "chat", + "im" + ], + "cordis": { + "service": {} + }, + "peerDependencies": { + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.1.1", + "@satorijs/plugin-im": "^0.0.0", + "@satorijs/plugin-im-utils": "^0.0.0" + }, + "devDependencies": { + "@cordisjs/client": "^0.1.12", + "@cordisjs/plugin-http": "^0.5.3", + "@cordisjs/plugin-webui": "^0.1.12", + "@satorijs/core": "^4.1.1", + "@satorijs/plugin-server": "^2.6.5" + }, + "dependencies": { + "cordis": "^3.17.7", + "cosmokit": "^1.6.2" + } } From 4179530791bb37dd0ee189f34fe3da0bc9377ed0 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Mon, 30 Sep 2024 20:50:17 +0800 Subject: [PATCH 49/50] chore: tweak --- packages/core/src/auth.ts | 2 +- packages/core/src/index.ts | 4 ++-- packages/core/src/notifier.ts | 2 +- .../im/client/app/messenger/editor/editor.vue | 2 +- .../im/client/app/messenger/message-item.vue | 4 ++-- .../client/app/messenger/message/code-span.vue | 4 +++- .../im/client/app/messenger/message/index.ts | 2 +- .../im/client/app/navigation/session-item.vue | 2 +- packages/im/client/app/scene/create-guild.vue | 2 +- packages/im/client/components/role/selector.vue | 16 ++++++++++------ 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 68e1125..4a7083a 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -4,7 +4,7 @@ import { Context, Dict, Service, Universal } from '@satorijs/core' import { Login, User } from './types' import { genId, validate } from '@satorijs/plugin-im-utils' -export class ImAuthService extends Service { +export default class ImAuthService extends Service { static inject = ['model', 'database'] public logins: Dict = {} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 524503b..0d50252 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,8 @@ import { Context, Service } from '@satorijs/core' import ImDatabase from './database' -import { ImAuthService } from './auth' +import ImAuthService from './auth' import ImDataService from './data' -import { ImEventService } from './notifier' +import ImEventService from './notifier' import { Event } from './types' export * as Im from './types' diff --git a/packages/core/src/notifier.ts b/packages/core/src/notifier.ts index 013bc6a..1d8f2a4 100644 --- a/packages/core/src/notifier.ts +++ b/packages/core/src/notifier.ts @@ -1,7 +1,7 @@ import { Context, Dict, Service } from '@satorijs/core' import { Event, Login } from './types' -export class ImEventService extends Service { +export default class ImEventService extends Service { static inject = ['database', 'im', 'im.auth'] private currentEventId: number // ? eventChans: Dict = {} diff --git a/packages/im/client/app/messenger/editor/editor.vue b/packages/im/client/app/messenger/editor/editor.vue index efad77c..3f77248 100644 --- a/packages/im/client/app/messenger/editor/editor.vue +++ b/packages/im/client/app/messenger/editor/editor.vue @@ -11,7 +11,7 @@
输入文字... -
+
diff --git a/packages/im/client/app/messenger/message/code-span.vue b/packages/im/client/app/messenger/message/code-span.vue index 6c6c384..98baa5e 100644 --- a/packages/im/client/app/messenger/message/code-span.vue +++ b/packages/im/client/app/messenger/message/code-span.vue @@ -11,7 +11,9 @@ const props = defineProps<{ diff --git a/packages/im/client/app/messenger/message/index.ts b/packages/im/client/app/messenger/message/index.ts index 8b2c000..abc1df7 100644 --- a/packages/im/client/app/messenger/message/index.ts +++ b/packages/im/client/app/messenger/message/index.ts @@ -48,7 +48,7 @@ function renderToken(token: Token): VNode { } return h(resolveComponent(name), { ...args }) } else if (token.type === 'code') { - return h('code', { content: token.text + '\n' }) + return h('code', token.text + '\n') } else if (token.type === 'codespan') { return h('code', { text: token.raw }) } else if (token.type === 'paragraph') { diff --git a/packages/im/client/app/navigation/session-item.vue b/packages/im/client/app/navigation/session-item.vue index d36a3d8..b6cc2e1 100644 --- a/packages/im/client/app/navigation/session-item.vue +++ b/packages/im/client/app/navigation/session-item.vue @@ -6,7 +6,7 @@ > -
+
diff --git a/packages/im/client/app/scene/create-guild.vue b/packages/im/client/app/scene/create-guild.vue index 113e72c..8790ff2 100644 --- a/packages/im/client/app/scene/create-guild.vue +++ b/packages/im/client/app/scene/create-guild.vue @@ -6,7 +6,7 @@ - + diff --git a/packages/im/client/components/role/selector.vue b/packages/im/client/components/role/selector.vue index 6dfeb09..955a51b 100644 --- a/packages/im/client/components/role/selector.vue +++ b/packages/im/client/components/role/selector.vue @@ -60,14 +60,18 @@ const chat = useContext()['im.client'] const selected = defineModel() +// HACK: Cannot use local variables in defineProps() due to hoisting. +// Manually setting 'except' default value. const props = defineProps<{ type: 'friend' | 'member' - except: string[] + except?: string[] gid?: string filterable?: boolean showResult?: boolean }>() +const except = props.except || [chat.getLogin().user!.id] + onMounted(() => { getItems().then(() => (loaded.value = true)) }) @@ -83,13 +87,13 @@ const items = ref>([]) const leftItems = computed(() => items.value.filter( (item) => - item.user.name!.includes(keyword.value) && - !props.except.find((value) => value === item.user.id) + item.user.name!.includes(keyword.value) && !except.find((value) => value === item.user.id) ) ) -const rightItems = computed(() => leftItems.value.filter((item) => item.active)) -watch(rightItems.value, (newValue) => { - selected.value = newValue.map((item) => item.user.id) +const rightItems = computed(() => { + const items = leftItems.value.filter((item) => item.active) + selected.value = items.map((item) => item.user.id) + return items }) async function getItems() { From e3838234a94d105e7574fd78b93e59c4a1d23963 Mon Sep 17 00:00:00 2001 From: ElectRICdll <2636422098@qq.com> Date: Sat, 26 Oct 2024 23:32:22 +0800 Subject: [PATCH 50/50] chore: update dependencies --- package.json | 71 +++++++++++++++++++------------------- packages/bot/package.json | 11 ++---- packages/core/package.json | 13 ++++--- packages/im/package.json | 2 ++ 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 03e9590..0b713fb 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,37 @@ { - "name": "@root/satori-im", - "private": true, - "type": "module", - "version": "1.0.0", - "workspaces": [ - "packages/*" - ], - "license": "MIT", - "scripts": { - "build": "yakumo build", - "lint": "eslint --cache", - "test": "yakumo mocha -r esbuild-register -t 10000", - "test:text": "shx rm -rf coverage && c8 -r text yarn test", - "test:json": "shx rm -rf coverage && c8 -r json yarn test", - "test:html": "shx rm -rf coverage && c8 -r html yarn test" - }, - "devDependencies": { - "@cordisjs/eslint-config": "^1.1.1", - "@types/chai": "^4.3.14", - "@types/mocha": "^9.1.1", - "@types/node": "^20.11.30", - "c8": "^7.14.0", - "chai": "^4.4.1", - "esbuild": "^0.18.20", - "esbuild-register": "^3.5.0", - "eslint": "^8.57.0", - "mocha": "^9.2.2", - "shx": "^0.3.4", - "typescript": "^5.4.3", - "yakumo": "^1.0.0-beta.16", - "yakumo-esbuild": "^1.0.0-beta.6", - "yakumo-mocha": "^1.0.0-beta.2", - "yakumo-tsc": "^1.0.0-beta.4", - "yml-register": "^1.2.5" - } + "name": "@root/satori-im", + "private": true, + "type": "module", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ], + "license": "MIT", + "scripts": { + "build": "yakumo build", + "lint": "eslint --cache", + "test": "yakumo mocha -r esbuild-register -t 10000", + "test:text": "shx rm -rf coverage && c8 -r text yarn test", + "test:json": "shx rm -rf coverage && c8 -r json yarn test", + "test:html": "shx rm -rf coverage && c8 -r html yarn test" + }, + "devDependencies": { + "@cordisjs/eslint-config": "^1.1.1", + "@types/chai": "^4.3.14", + "@types/mocha": "^9.1.1", + "@types/node": "^20.11.30", + "c8": "^7.14.0", + "chai": "^4.4.1", + "esbuild": "^0.18.20", + "esbuild-register": "^3.5.0", + "eslint": "^8.57.0", + "mocha": "^9.2.2", + "shx": "^0.3.4", + "typescript": "^5.4.3", + "yakumo": "^1.0.0-beta.16", + "yakumo-esbuild": "^1.0.0-beta.6", + "yakumo-mocha": "^1.0.0-beta.2", + "yakumo-tsc": "^1.0.0-beta.4", + "yml-register": "^1.2.5" } - \ No newline at end of file +} diff --git a/packages/bot/package.json b/packages/bot/package.json index a764997..178328d 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -29,17 +29,12 @@ "adapter" ], "peerDependencies": { - "@cordisjs/plugin-webui": "^0.1.12", "@satorijs/core": "^4.2.6", - "@satorijs/plugin-im": "^0.0.0", - "@satorijs/plugin-server": "^2.7.1" + "@satorijs/plugin-im": "^0.0.0" }, "devDependencies": { "@cordisjs/client": "^0.1.12", - "@cordisjs/plugin-webui": "^0.1.12", - "@satorijs/core": "^4.2.6" - }, - "dependencies": { - "cordis": "^3.18.0" + "@satorijs/core": "^4.2.6", + "@satorijs/plugin-im": "^0.0.0" } } diff --git a/packages/core/package.json b/packages/core/package.json index a4302e2..683bbe5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,20 +39,19 @@ } }, "peerDependencies": { - "@satorijs/core": "^4.2.6", - "@satorijs/plugin-im-utils": "^0.0.0" + "minato": "^3.5.0" }, "devDependencies": { "@cordisjs/client": "^0.1.12", "@cordisjs/plugin-server": "^0.2.3", "@satorijs/core": "^4.2.6", - "@types/mime-types": "^2" + "@types/mime-types": "^2", + "minato": "^3.5.0" }, "dependencies": { - "@minatojs/driver-mysql": "^3.5.0", - "cordis": "^3.18.1", + "@satorijs/core": "^4.2.6", + "@satorijs/plugin-im-utils": "^0.0.0", "cosmokit": "^1.6.2", - "mime-types": "^2.1.35", - "minato": "^3.5.0" + "mime-types": "^2.1.35" } } diff --git a/packages/im/package.json b/packages/im/package.json index d216741..a726a85 100644 --- a/packages/im/package.json +++ b/packages/im/package.json @@ -42,9 +42,11 @@ "@cordisjs/plugin-http": "^0.5.3", "@cordisjs/plugin-webui": "^0.1.12", "@satorijs/core": "^4.1.1", + "@satorijs/plugin-im": "^0.0.0", "@satorijs/plugin-server": "^2.6.5" }, "dependencies": { + "@satorijs/plugin-im-utils": "^0.0.0", "cordis": "^3.17.7", "cosmokit": "^1.6.2" }