-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
11d203d
commit c557176
Showing
10 changed files
with
448 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.使用密码登录-(滑动验证码教程)> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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}!`) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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() | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { 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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { GuildApp } from "./app" | ||
export { Guild, GuildRole, GuildMember } from "./guild" | ||
export { Channel, NotifyType, ChannelType } from "./channel" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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) | ||
} |
Oops, something went wrong.