Skip to content

Commit

Permalink
first release
Browse files Browse the repository at this point in the history
  • Loading branch information
takayama-lily committed May 11, 2022
1 parent 11d203d commit c557176
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 32 deletions.
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
# oicq-guild

In development
[oicq](https://github.com/takayama-lily/oicq) guild plugin

**Install:**

```bash
# edit test.js with your account and password
npm i typescript -g
npm i
npm test
npm i oicq-guild
```

**how to clear the slider:**
**Usage:**

<https://github.com/takayama-lily/oicq/wiki/01.使用密码登录-(滑动验证码教程)>
```js
const { createClient } = require("oicq")
const { GuildApp } = require("oicq-guild")

// input with your account and password
const account = 0
const password = ""

// create oicq client
const client = createClient(account)
client.login(password)

// create guild app and bind it to an oicq client
const app = GuildApp.bind(client)

**how to analyze the hex:**
app.on("ready", function () {
console.log("My guild list:")
console.log(this.guilds)
})

<https://wife.awa.moe/unpack-tools/> or
<https://protobuf-decoder.netlify.app/>
app.on("message", e => {
console.log(e)
if (e.raw_message === "hello")
e.reply(`Hello, ${e.sender.nickname}!`)
})
```

**how to clear the slider captcha:**

<https://github.com/takayama-lily/oicq/wiki/01.使用密码登录-(滑动验证码教程)>
25 changes: 25 additions & 0 deletions demo/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use strict"
const { createClient } = require("oicq")
const { GuildApp } = require("../lib/index")

const account = 0
const password = ""

const client = createClient(account)
client.on("system.login.slider", function (e) {
console.log("input ticket:")
process.stdin.once("data", ticket => this.submitSlider(String(ticket).trim()))
}).login(password)

const app = GuildApp.bind(client)

app.on("ready", function () {
console.log("My guild list:")
console.log(this.guilds)
})

app.on("message", e => {
console.log(e)
if (e.raw_message === "hello")
e.reply(`Hello, ${e.sender.nickname}!`)
})
89 changes: 89 additions & 0 deletions lib/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import EventEmitter from "events"
import { Client, ApiRejection } from "oicq"
import { pb } from "oicq/lib/core"
import { lock, NOOP, log } from "oicq/lib/common"
import { onFirstView, onGroupProMsg } from "./internal"
import { Guild } from "./guild"
import { GuildMessage } from "./message"

declare module "oicq" {
export interface Client {
sendOidbSvcTrpcTcp: (cmd: string, body: Uint8Array) => Promise<pb.Proto>
}
}

Client.prototype.sendOidbSvcTrpcTcp = async function (cmd: string, body: Uint8Array) {
const sp = cmd //OidbSvcTrpcTcp.0xf5b_1
.replace("OidbSvcTrpcTcp.", "")
.split("_");
const type1 = parseInt(sp[0], 16), type2 = parseInt(sp[1]);
body = pb.encode({
1: type1,
2: type2,
4: body,
6: "android " + this.apk.ver,
})
const payload = await this.sendUni(cmd, body)
log(payload)
const rsp = pb.decode(payload)
if (rsp[3] === 0) return rsp[4]
throw new ApiRejection(rsp[3], rsp[5])
}

export interface GuildApp {
on(event: "ready", listener: (this: this) => void): this;
on(event: "message", listener: (this: this, e: GuildMessage) => void): this;
once(event: "ready", listener: (this: this) => void): this;
once(event: "message", listener: (this: this, e: GuildMessage) => void): this;
off(event: "ready", listener: (this: this) => void): this;
off(event: "message", listener: (this: this, e: GuildMessage) => void): this;
}

/** 获取应用程序入口 */
export class GuildApp extends EventEmitter {

protected readonly c: Client

/** 我的频道id */
tiny_id = ""

/** 我加入的频道列表 */
guilds = new Map<string, Guild>()

/** 获得所属的客户端对象 */
get client() {
return this.c
}

protected constructor(client: Client) {
super()
client.on("internal.sso", (cmd: string, payload: Buffer) => {
if (cmd === "trpc.group_pro.synclogic.SyncLogic.PushFirstView")
onFirstView.call(this, payload)
else if (cmd === "MsgPush.PushGroupProMsg")
onGroupProMsg.call(this, payload)
})
client.on("system.online", _ => this.tiny_id = client.tiny_id)
this.c = client
lock(this, "c")
}

/** 绑定QQ客户端 */
static bind(client: Client) {
return new GuildApp(client)
}

/** 重新加载频道列表 */
reloadGuilds(): Promise<void> {
this.c.sendUni("trpc.group_pro.synclogic.SyncLogic.SyncFirstView", pb.encode({ 1: 0, 2: 0, 3: 0 })).then(payload => {
this.tiny_id = String(pb.decode(payload)[6])
}).catch(NOOP)
return new Promise((resolve, reject) => {
const id = setTimeout(reject, 5000)
this.once("ready", () => {
clearTimeout(id)
resolve()
})
})
}
}
83 changes: 83 additions & 0 deletions lib/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { randomBytes } from "crypto"
import { pb } from "oicq/lib/core"
import { lock } from "oicq/lib/common"
import { Sendable, Converter } from "oicq/lib/message"
import { ApiRejection } from "oicq"
import { Guild } from "./guild"

export enum NotifyType {
Unknown = 0,
AllMessages = 1,
Nothing = 2,
}

export enum ChannelType {
Unknown = 0,
Text = 1,
Voice = 2,
Live = 5,
App = 6,
Forum = 7,
}

export class Channel {

channel_name = ""
channel_type = ChannelType.Unknown
notify_type = NotifyType.Unknown

constructor(public readonly guild: Guild, public readonly channel_id: string) {
lock(this, "guild")
lock(this, "channel_id")
}

_renew(channel_name: string, notify_type: NotifyType, channel_type: ChannelType) {
this.channel_name = channel_name
this.notify_type = notify_type
this.channel_type = channel_type
}

/**
* 发送频道消息
* 暂时仅支持发送: 文本、AT、表情
*/
async sendMessage(content: Sendable): Promise<{ seq: number, rand: number, time: number}> {
const payload = await this.guild.app.client.sendUni("MsgProxy.SendMsg", pb.encode({
1: {
1: {
1: {
1: BigInt(this.guild.guild_id),
2: Number(this.channel_id),
3: this.guild.app.client.uin
},
2: {
1: 3840,
3: randomBytes(4).readUInt32BE()
}
},
3: {
1: new Converter(content).rich
}
}
}))
const rsp = pb.decode(payload)
if (rsp[1])
throw new ApiRejection(rsp[1], rsp[2])
return {
seq: rsp[4][2][4],
rand: rsp[4][2][3],
time: rsp[4][2][6],
}
}

/** 撤回频道消息 */
async recallMessage(seq: number): Promise<boolean> {
const body = pb.encode({
1: BigInt(this.guild.guild_id),
2: Number(this.channel_id),
3: Number(seq)
})
await this.guild.app.client.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0xf5e_1", body)
return true
}
}
103 changes: 103 additions & 0 deletions lib/guild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { pb } from "oicq/lib/core"
import { lock } from "oicq/lib/common"
import { GuildApp } from "./app"
import { Channel} from "./channel"

export enum GuildRole {
Member = 1,
GuildAdmin = 2,
Owner = 4,
ChannelAdmin = 5,
}

export interface GuildMember {
tiny_id: string
card: string
nickname: string
role: GuildRole
join_time: number
}

const members4buf = pb.encode({
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
})

export class Guild {

guild_name = ""
channels = new Map<string, Channel>()

constructor(public readonly app: GuildApp, public readonly guild_id: string) {
lock(this, "app")
lock(this, "guild_id")
}

_renew(guild_name: string, proto: pb.Proto | pb.Proto[]) {
this.guild_name = guild_name
if (!Array.isArray(proto))
proto = [proto]
const tmp = new Set<string>()
for (const p of proto) {
const id = String(p[1]), name = String(p[8]),
notify_type = p[7], channel_type = p[9]
tmp.add(id)
if (!this.channels.has(id))
this.channels.set(id, new Channel(this, id))
const channel = this.channels.get(id)!
channel._renew(name, notify_type, channel_type)
}
for (let [id, _] of this.channels) {
if (!tmp.has(id))
this.channels.delete(id)
}
}

/** 获取频道成员列表 */
async getMemberList() {
let index = 0 // todo member count over 500
const body = pb.encode({
1: BigInt(this.guild_id),
2: 3,
3: 0,
4: members4buf,
6: index,
8: 500,
14: 2,
})
const rsp = await this.app.client.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0xf5b_1", body)
const list: GuildMember[] = []
const members = Array.isArray(rsp[5]) ? rsp[5] : [rsp[5]]
const admins = Array.isArray(rsp[25]) ? rsp[25] : [rsp[25]]
for (const p of admins) {
const role = p[1] as GuildRole
const m = Array.isArray(p[2]) ? p[2] : [p[2]]
for (const p2 of m) {
list.push({
tiny_id: String(p2[8]),
card: String(p2[2]),
nickname: String(p2[3]),
role,
join_time: p2[4],
})
}

}
for (const p of members) {
list.push({
tiny_id: String(p[8]),
card: String(p[2]),
nickname: String(p[3]),
role: GuildRole.Member,
join_time: p[4],
})
}
return list
}
}
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { GuildApp } from "./app"
export { Guild, GuildRole, GuildMember } from "./guild"
export { Channel, NotifyType, ChannelType } from "./channel"
38 changes: 38 additions & 0 deletions lib/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { GuildApp } from "./app"
import { pb } from "oicq/lib/core"
import { Guild } from "./guild"
import { GuildMessage } from "./message"

export function onFirstView(this: GuildApp, payload: Buffer) {
const proto = pb.decode(payload)
if (!proto[3]) return
if (!Array.isArray(proto[3])) proto[3] = [proto[3]]
const tmp = new Set<string>()
for (let p of proto[3]) {
const id = String(p[1]), name = String(p[4])
tmp.add(id)
if (!this.guilds.has(id))
this.guilds.set(id, new Guild(this, id))
const guild = this.guilds.get(id)!
guild._renew(name, p[3])
}
for (let [id, _] of this.guilds) {
if (!tmp.has(id))
this.guilds.delete(id)
}
this.client.logger.mark(`[Guild] 加载了${this.guilds.size}个频道`)
this.emit("ready")
}

export function onGroupProMsg(this: GuildApp, payload: Buffer) {
try {
var msg = new GuildMessage(pb.decode(payload))
} catch {
return
}
this.client.logger.info(`[Guild: ${msg.guild_name}, Member: ${msg.sender.nickname}]` + msg.raw_message)
const channel = this.guilds.get(msg.guild_id)?.channels.get(msg.channel_id)
if (channel)
msg.reply = channel.sendMessage.bind(channel)
this.emit("message", msg)
}
Loading

0 comments on commit c557176

Please sign in to comment.