oicq-icalingua-plus-plus 1.22.13 → 1.22.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -934,6 +934,7 @@ export class Client extends EventEmitter {
934
934
 
935
935
  /** 私聊 */
936
936
  _sendPrivateMsg(user_id: number, message: Sendable, auto_escape?: boolean): Promise<Ret<{ message_id: string }>>;
937
+ sendFile(user_id: number, file: MediaFile, name?: string, process?: (percentage: string) => void): Promise<Ret<{ message_id: string }>>;
937
938
  /** 群聊 */
938
939
  sendGroupMsg(group_id: number, message: Sendable, auto_escape?: boolean): Promise<Ret<{ message_id: string }>>;
939
940
  /** 群临时会话,大多数时候可以使用私聊达到同样效果 */
package/lib/client.js CHANGED
@@ -351,6 +351,9 @@ class Client extends EventEmitter {
351
351
  _sendPrivateMsg(user_id, message = "", auto_escape = false) {
352
352
  return this._useProtocol(chat.sendMsg, [user_id, message, auto_escape, 0]);
353
353
  }
354
+ sendFile(user_id, file, name, process) {
355
+ return this._useProtocol(chat.sendFile, [user_id, file, name, process]);
356
+ }
354
357
  sendGroupMsg(group_id, message = "", auto_escape = false) {
355
358
  return this._useProtocol(chat.sendMsg, [group_id, message, auto_escape, 1]);
356
359
  }
@@ -161,10 +161,11 @@ class Builder {
161
161
  * @param {number} target
162
162
  * @param {0|1|2} type //0私聊 1群聊 2讨论组
163
163
  */
164
- constructor(c, target, type) {
164
+ constructor(c, target, type, file = null) {
165
165
  this.c = c;
166
166
  this.target = target;
167
167
  this.type = type;
168
+ this.file = file;
168
169
  }
169
170
 
170
171
  /**
@@ -845,7 +846,7 @@ class Builder {
845
846
  if (tasks.length > 0)
846
847
  var rsp = await Promise.race(tasks);
847
848
 
848
- if (!this.elems.length) {
849
+ if (!this.elems.length && this.file === null) {
849
850
  if (rsp) return rsp;
850
851
  throw new Error("empty message");
851
852
  }
@@ -867,6 +868,9 @@ class Builder {
867
868
  } else {
868
869
  let user_id = this.target;
869
870
  routing = { 1: { 1: user_id } };
871
+ if (this.file !== null) {
872
+ routing = { 15: { 1: user_id, 2: 4 } };
873
+ }
870
874
  if (this.c.sl.has(user_id)) {
871
875
  try {
872
876
  const group_id = this.c.sl.get(user_id).group_id;
@@ -910,7 +914,7 @@ class Builder {
910
914
  return pb.encode({
911
915
  1: this.routing,
912
916
  2: content,
913
- 3: { 1: rich },
917
+ 3: this.file !== null ? this.file : { 1: rich },
914
918
  4: this.seq,
915
919
  5: this.random,
916
920
  6: this.type > 0 ? null : this.c.buildSyncCookie(),
@@ -8,8 +8,11 @@ const { getC2CMsgs, getGroupMsgs, getLastSeq } = require("./history");
8
8
  const { parseC2CMsg, parseGroupMsg, parseForwardMsg } = require("./parser");
9
9
  const common = require("../common");
10
10
  const pb = require("../algo/pb");
11
+ const fs = require("fs");
12
+ const path = require("path");
11
13
  const { parseC2CMessageId, parseGroupMessageId, genMessageUuid, genRandom } = common;
12
14
  const { ImageBuilder, uploadImages, buildImageFileParam } = require("./image");
15
+ const { highwayHttpUpload } = require("../service");
13
16
 
14
17
  //send msg----------------------------------------------------------------------------------------------------
15
18
 
@@ -47,6 +50,119 @@ function sendMsg(target, message, escape, type) {
47
50
  return builder.buildAndSend(message, escape);
48
51
  }
49
52
 
53
+ async function sendFile(user_id, file, filename, callback) {
54
+ let filesize, filemd5, filesha, filestream
55
+ if (file instanceof Uint8Array) {
56
+ if (!Buffer.isBuffer(file))
57
+ file = Buffer.from(file)
58
+ filesize = file.length
59
+ filemd5 = md5(file), filesha = sha(file)
60
+ filename = filename ? String(filename) : ("file" + filemd5.toString("hex"))
61
+ filestream = Readable.from(file, { objectMode: false, highWaterMark: 524288 })
62
+ } else {
63
+ file = String(file)
64
+ filesize = (await fs.promises.stat(file)).size
65
+ ;[filemd5, filesha] = await common.fileHash(file)
66
+ filename = filename ? String(filename) : path.basename(file)
67
+ filestream = fs.createReadStream(file, { highWaterMark: 524288 })
68
+ }
69
+ const body1700 = pb.encode({
70
+ 1: 1700,
71
+ 2: 6,
72
+ 19: {
73
+ 10: this.uin,
74
+ 20: user_id,
75
+ 30: filesize,
76
+ 40: filename,
77
+ 50: filemd5,
78
+ 60: filesha,
79
+ 70: "/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/" + filename,
80
+ 80: 0,
81
+ 90: 0,
82
+ 100: 0,
83
+ 110: filemd5,
84
+ },
85
+ 101: 3,
86
+ 102: 104,
87
+ 200: 1,
88
+ })
89
+ const payload = await this.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_UPLOAD_V3-1700", body1700)
90
+ const rsp1700 = pb.decode(payload)[19]
91
+
92
+ if (rsp1700[10] !== 0)
93
+ throw new Error(`${rsp1700[10]}, ${rsp1700[20]}`)
94
+
95
+ const fid = rsp1700[90].toBuffer()
96
+
97
+ if (!rsp1700[110]) {
98
+ const ext = pb.encode({
99
+ 1: 100,
100
+ 2: 2,
101
+ 100: {
102
+ 100: {
103
+ 1: 3,
104
+ 100: this.uin,
105
+ 200: user_id,
106
+ 400: 0,
107
+ 700: payload,
108
+ },
109
+ 200: {
110
+ 100: filesize,
111
+ 200: filemd5,
112
+ 300: filesha,
113
+ 400: filemd5,
114
+ 600: fid,
115
+ 700: rsp1700[220].toBuffer(),
116
+ },
117
+ 300: {
118
+ 100: 2,
119
+ 200: String(this.apk.subid),
120
+ 300: 2,
121
+ 400: "d92615c5",
122
+ 600: 4,
123
+ },
124
+ 400: {
125
+ 100: filename,
126
+ },
127
+ },
128
+ 200: 1
129
+ })
130
+ await highwayHttpUpload.call(this, filestream, {
131
+ md5: filemd5,
132
+ size: filesize,
133
+ cmdid: 69, // CmdID.OfflineFile,
134
+ ext, callback
135
+ })
136
+ }
137
+
138
+ const body800 = pb.encode({
139
+ 1: 800,
140
+ 2: 7,
141
+ 10: {
142
+ 10: this.uin,
143
+ 20: user_id,
144
+ 30: fid,
145
+ },
146
+ 101: 3,
147
+ 102: 104,
148
+ })
149
+ await this.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_UPLOAD_SUCC-800", body800)
150
+ const proto3 = {
151
+ 2: {
152
+ 1: {
153
+ 1: 0,
154
+ 3: fid,
155
+ 4: filemd5,
156
+ 5: filename,
157
+ 6: filesize,
158
+ 9: 1,
159
+ }
160
+ }
161
+ }
162
+ const builder = new Builder(this, user_id, 0, proto3);
163
+ return builder.buildAndSend('', false);
164
+ }
165
+
50
166
  //recall----------------------------------------------------------------------------------------------------
51
167
 
52
168
  /**
@@ -272,5 +388,5 @@ async function preloadImages(files = []) {
272
388
  module.exports = {
273
389
  sendMsg, sendTempMsg, recallMsg, reportReaded,
274
390
  getOneMsg, getMsgs, getForwardMsg,
275
- preloadImages
391
+ preloadImages, sendFile
276
392
  };
package/lib/service.js CHANGED
@@ -12,6 +12,7 @@ const { randomBytes } = require("crypto");
12
12
  const tea = require("./algo/tea");
13
13
  const pb = require("./algo/pb");
14
14
  const { md5, NOOP, BUF0} = require("./common");
15
+ const axios = require("axios").default;
15
16
  const MAX_UPLOAD_SIZE = 31457280;
16
17
 
17
18
  /**
@@ -150,6 +151,119 @@ function highwayUploadStream(readable, obj, ip, port) {
150
151
  });
151
152
  }
152
153
 
154
+ const agent = new http.Agent({ maxSockets: 10 })
155
+
156
+ const __ = Buffer.from([41])
157
+
158
+ function highwayHttpUpload(readable, obj) {
159
+ const ip = this.storage.ip
160
+ const port = this.storage.port
161
+ if (!port) throw new Error("没有上传通道,如果你刚刚登录,请等待几秒")
162
+
163
+ console.log(`highway(http) ip:${ip} port:${port}`)
164
+ const url = "http://" + ip + ":" + port + "/cgi-bin/httpconn?htcmd=0x6FF0087&uin=" + this.uin
165
+ let seq = 1
166
+ let offset = 0, limit = 524288
167
+ obj.ticket = this.storage.sig_session
168
+
169
+ const tasks = new Set()
170
+ const cancels = new Set()
171
+ let finished = 0
172
+
173
+ readable.on("data", data => {
174
+ let _offset = 0
175
+ while (_offset < data.length) {
176
+ const chunk = data.slice(_offset, limit + _offset)
177
+ const head = pb.encode({
178
+ 1: {
179
+ 1: 1,
180
+ 2: String(this.uin),
181
+ 3: "PicUp.DataUp",
182
+ 4: seq++,
183
+ 5: 0,
184
+ 6: this.apk.subid,
185
+ 8: obj.cmdid,
186
+ },
187
+ 2: {
188
+ 1: 0,
189
+ 2: obj.size,
190
+ 3: offset + _offset,
191
+ 4: chunk.length,
192
+ 6: obj.ticket,
193
+ 8: md5(chunk),
194
+ 9: obj.md5,
195
+ 10: 0,
196
+ 13: 0,
197
+ },
198
+ 3: obj.ext,
199
+ 4: Date.now()
200
+ })
201
+ _offset += chunk.length
202
+ const _ = Buffer.allocUnsafe(9)
203
+ _.writeUInt8(40)
204
+ _.writeUInt32BE(head.length, 1)
205
+ _.writeUInt32BE(chunk.length, 5)
206
+ const buf = Buffer.concat([_, head, chunk, __])
207
+ const task = new Promise((resolve, reject) => {
208
+ const c = axios.CancelToken.source()
209
+ cancels.add(c)
210
+ axios.post(url, buf, {
211
+ responseType: "arraybuffer",
212
+ httpAgent: agent,
213
+ cancelToken: c.token,
214
+ headers: {
215
+ "Content-Length": String(buf.length)
216
+ }
217
+ }).then(r => {
218
+ let percentage, rsp
219
+ try {
220
+ const buf = Buffer.from(r?.data)
221
+ const header = buf.slice(9, buf.length - 1)
222
+ rsp = pb.decode(header)
223
+ } catch (err) {
224
+ this.logger.error(err)
225
+ reject(err)
226
+ return
227
+ }
228
+ if (rsp?.[3] !== 0) {
229
+ reject(`${rsp[3]}, unknown highway error"`)
230
+ return
231
+ }
232
+ ++finished
233
+ percentage = (finished / tasks.size * 100).toFixed(2)
234
+ this.logger.debug(`highway(http) chunk uploaded (${percentage}%)`)
235
+ if (typeof obj.callback === "function" && percentage)
236
+ obj.callback(percentage)
237
+ if (finished < tasks.size && rsp[7]?.toBuffer().length > 0) {
238
+ cancels.forEach(c => c.cancel())
239
+ this.logger.debug(`highway(http) chunk uploaded (100.00%)`)
240
+ if (typeof obj.callback === "function")
241
+ obj.callback("100.00")
242
+ }
243
+ if (finished >= tasks.size && !rsp[7]?.toBuffer().length)
244
+ reject("文件校验未通过,上传失败")
245
+ resolve(undefined)
246
+ }).catch(reject)
247
+ })
248
+ tasks.add(task)
249
+ }
250
+ offset += data.length
251
+ })
252
+
253
+ return new Promise((resolve, reject) => {
254
+ readable.on("err", reject)
255
+ .on("end", () => {
256
+ Promise.all(tasks).then(resolve).catch(err => {
257
+ if (err instanceof axios.Cancel === false) {
258
+ cancels.forEach(c => c.cancel())
259
+ reject(err)
260
+ }
261
+ resolve(undefined)
262
+ })
263
+ })
264
+ })
265
+ }
266
+
153
267
  const ERROR_SIZE_TOO_BIG = new Error("文件体积超过30MB,拒绝下载");
154
268
 
155
269
  class DownloadTransform extends stream.Transform {
@@ -200,5 +314,5 @@ function downloadFromWeb(url, headers, redirect = 0) {
200
314
  }
201
315
 
202
316
  module.exports = {
203
- downloadFromWeb, highwayUploadStream, int32ip2str, MAX_UPLOAD_SIZE,
317
+ downloadFromWeb, highwayUploadStream, int32ip2str, highwayHttpUpload, MAX_UPLOAD_SIZE,
204
318
  };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "oicq-icalingua-plus-plus",
3
3
  "app_name": "oicq-icalingua-plus-plus",
4
- "version": "1.22.13",
5
- "app_version": "1.22.13",
6
- "upday": "2022/12/14",
4
+ "version": "1.22.14",
5
+ "app_version": "1.22.14",
6
+ "upday": "2022/12/24",
7
7
  "protocol_version": "v11",
8
8
  "description": "oicq(https://github.com/takayama-lily/oicq) v1 for Icalingua++",
9
9
  "main": "index.js",
@@ -21,6 +21,7 @@
21
21
  "author": "The Icalingua++ Authors",
22
22
  "license": "MPL-2.0",
23
23
  "dependencies": {
24
+ "axios": "^1.2.1",
24
25
  "jsqr": "^1.4.0",
25
26
  "log4js": "^6.3.0",
26
27
  "long": "^4.0.0",