0%

Twitter监控并翻译转发到qq群

经常需要往沙雕复读群里面发舰c最新Twitter,由于复制翻译转发太过麻烦,于是考虑写一个脚本直接完成此操作。本脚本分为三部分进行,第一部分 Twitter官推的监控,做到获取最新的Twitter信息,第二部分 文本翻译,这里计划使用Google translate的api进行,第三部分发送到qq,这里考虑使用github上面的开源项目进行发送。

Twitter api可以很快速的监控,但是。。。三个账号都没申请到Twitter开发者账户!!!
不想直接爬Twitter开发者账户的内容,很容易炸。。。
求开发者账户!!!

换了一家新公司,和同事聊天时白嫖到了推特开发者账户,项目重启,let’s go!
作为自己玩的东西,我的宗旨是能少花钱 就不多花钱,能不花钱就不花钱。最近这两年腾讯阿里都增加了云函数的服务,送了很多免费额度,所以就基于云函数来实现整套系统的需求。
因为云函数是按照使用内存和使用时间收费的,这里使用异步处理会比较好,所以选择了使用nodejs实现了整个脚本。
整个流程的处理逻辑比较简单,从Twitter拉取数据,发送给qq即可。
但是细小的问题比较多,我这边一一整理。

Twitter的认证及消息获取

因为Twitter的api现在都必须进过认证,所以我们选用OAuth进行认证。这里直接使用oauth模块处理即可。
然后调用用户时间线的api(https://api.twitter.com/1.1/statuses/user_timeline.json) 上传since_id和user_id获取即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const OAuth = require('oauth');
const oauth = new OAuth.OAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
'secretId',
'secretKey',
'1.0A',
null,
'HMAC-SHA1'
);
async function getTwitterRes(data) {
const result = new Promise(resolve => {
const url = 'https://api.twitter.com/1.1/statuses/user_timeline.json?since_id=' + data.since_id + '&user_id=' + data.user_id;
oauth.get(
url,
'token', //test user token
'secret', //test user secret
function (e, data, res){
if (e) console.error(e);
resolve(JSON.parse(data));
}
);
})
return result;
}

腾讯云对象存储处理

每次请求需要获取当前已查询的最大since_id,就需要一个地方来保存。
前面说过了 能用免费的 不用付费的。
这里使用腾讯云对象进行数据存储。【这里有条件的可以使用数据库或其他方法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function getFileData(remotePath) {
let result = new Promise(res=>{
cos.getObject({
Bucket: 's************2', /* 必须 */
Region: 'ap-hongkong', /* 必须 */
Key: remotePath, /* 必须 */
}, function(err, data) {
// console.log(err || data);
res(data);
});
});
return result;
}
async function saveFileData(remotePath, data) {
let result = new Promise(res=>{
cos.putObject({
Bucket: 's***********2',
Region: 'ap-hongkong',
Key: remotePath, // 这里传入前缀
Body: JSON.stringify(data),
}, function (err, data) {
res(data);
console.log(err || data);
});
});
return result;
}

短链接处理

在我的需求中,发送QQ的时候还需要把原推特网址带上。
因为原网址太长了,这里使用短连接处理来缩短链接长度,之前我都是使用微博短链接、百度云短链接。
后来微博封了,百度云收费了。我一气之下自己搞了个短链接,这里就使用自己的短链接进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function processShortUrl(Longurl) {
const result = new Promise(resolve => {
const sendData = {
"LongUrl": Longurl,
"TermOfValidity": "1-year",
}
request({
url: "http://t.ipoi.bid/v1/create",
method: "POST",
json: true,
headers: {
"Token": "s**********************9",
"content-type": "application/json",
},
body: sendData
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body.ShortUrl);
};
});
});
return result;
}

文本翻译

最开始打算是使用谷歌翻译的,但是直接扒网页的容易被封。所以最后使用了腾讯云的机器翻译。
腾讯云甚至可以直接通过demo生成代码,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const tencentcloud = require("tencentcloud-sdk-nodejs");


const TmtClient = tencentcloud.tmt.v20180321.Client;
const models = tencentcloud.tmt.v20180321.Models;

const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;

let cred = new Credential("SecretId", "SecretKey");
let httpProfile = new HttpProfile();
httpProfile.endpoint = "tmt.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new TmtClient(cred, "ap-hongkong", clientProfile);

let req = new models.TextTranslateRequest();

let params = '{"SourceText":"本日実施「艦これ」稼働全サーバ群、令和二年初の重メンテナンス&アップデート、全作業完了しています。更新や接続などが不安定な場合は、ブラウザのキャッシュを削除後、再接続をお試しください。\\\\n提督の皆さん、大変お待たせしました!\\\\n今冬の… https://t.co/342hpoYLAv","Source":"jp","Target":"zh","ProjectId":0}'
req.from_json_string(params);

client.TextTranslate(req, function(errMsg, response) {

if (errMsg) {
console.log(errMsg);
return;
}

console.log(typeof response.to_json_string());
});

信息发送

在window服务器中启动酷q服务,然后使用http api插件,这里不过多阐述。
接受远端linux系统发来的消息,从而实现远程发送消息的目的。
这里自己写了个发送模块,只需要主函数导入模块使用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'use strict';
const request = require('request');

class PushQQ {
constructor(qurl, qtoken) {
this.qurl = qurl;
this.qtoken = qtoken;
}

async push_message(message_type, to_number, messages) {
try {
const result = new Promise(resolve => {
const sendData = {
"message_type": message_type,
"user_id": to_number,
"group_id": to_number,
"discuss_id": to_number,
"message": messages,
"auto_escape": "true",
};
request({
url: this.qurl + 'send_msg',
method: "POST",
json: true,
headers: {
"Authorization": "Bearer " + this.qtoken,
"content-type": "application/json",
},
body: sendData
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body);
};
});
})
return result;
} catch (e) {
console.log(e);
}
}
}
module.exports.PushQQ = PushQQ;

最终代码如下

config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"TwitterId": [
{
"userid": "997786053124616192",
"model": ["qq","Email","Telegram","WeChat"],
"qq": [
{
"qq_model": "group",
"qq_num": "498****55"
}
]
}
]
}

cos.json
腾讯云存储最大since_id 首次需要手动上传

1
2
3
{
"997786053124616192": "1198268744021102593"
}

index.js
主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
'use strict';
const pushmodel = require('./pushmodel');
const request = require('request');
const COS = require('cos-nodejs-sdk-v5');
const tencentcloud = require("tencentcloud-sdk-nodejs");
const cos = new COS({
SecretId: 'SecretId',
SecretKey: 'SecretKey'
});
const OAuth = require('oauth');
const oauth = new OAuth.OAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
'SecretId',
'SecretKey',
'1.0A',
null,
'HMAC-SHA1'
);

exports.main_handler = async (event, context, callback) => {
try {
//0、 初始化 获取最新的since_id
let [PQ,config,cosjson] = await Promise.all([
new pushmodel.PushQQ('http://*******:****/', 'J*************5'),
require('./config'),
JSON.parse((await getFileData('TwitterToQQ/cos.json')).Body.toString()),
]);
let cosjsonSaveFlag = false;
//异步处理每一个Twitter用户
await Promise.all(config.TwitterId.map(async item=>{
//1、 获取Twitter
const twitterRes = await getTwitterRes({since_id: cosjson[item.userid] , user_id: item.userid});
// console.log(twitterRes);
// 2、拆分处理每一条Twitter
if (twitterRes && twitterRes.length){
cosjsonSaveFlag = true;
cosjson[item.userid] = twitterRes[0].id_str;
await Promise.all(twitterRes.map(async item1=>{
//3、 文本处理 及 翻译
const sendData = await processTweet(item1);
//4、 异步处理每一个 发送qq
if (item.model.indexOf('qq')!== -1) {
await Promise.all(item.qq.map(async item2 => {
await PQ.push_message(item2.qq_model, item2.qq_num, sendData);
}));
}
}));
}
}));
// 保存最后更新的since_id
if (cosjsonSaveFlag) {
await saveFileData('TwitterToQQ/cos.json',cosjson);
}
return ;
} catch (e) {
console.log(e);
}
};

async function processTweet(data) {
let result = "";
result+="[" + data.user.name + "]\n";
result+="[原推特链接 " + await processShortUrl('https://twitter.com/' + data.user.id_str + '/status/' + data.id_str) + "]\n";
result+="[" + (new Date(Date.parse(data.created_at))).toString() + "]\n\n";
result+=data.text ;
if (data.quoted_status) {
result+="\n\n[转发自"+data.quoted_status.user.name+"]\n";
result+=data.quoted_status.text;
}
if (["997************92","294**********17"].indexOf(data.user.id_str)!== -1){
result+= "\n\n【日文推特翻译】\n";
result+=await processTranslate({
"data": data.text,
"Source": 'jp',
"Target": 'zh',
});
}
return result;
}

async function processTranslate(data) {
const result = new Promise(resolve => {
const TmtClient = tencentcloud.tmt.v20180321.Client;
const models = tencentcloud.tmt.v20180321.Models;

const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;

let cred = new Credential("SecretId", "SecretKey");
let httpProfile = new HttpProfile();
httpProfile.endpoint = "tmt.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new TmtClient(cred, "ap-hongkong", clientProfile);

let req = new models.TextTranslateRequest();
let params = JSON.stringify({
"SourceText": data.data,
"Source":data.Source,
"Target":data.Target,
"ProjectId":0
});
req.from_json_string(params);

client.TextTranslate(req, function(errMsg, response) {

if (errMsg) {
console.log(errMsg);
return;
}

resolve(JSON.parse(response.to_json_string()).TargetText);
});

});
return result;
}

async function processShortUrl(Longurl) {
const result = new Promise(resolve => {
const sendData = {
"LongUrl": Longurl,
"TermOfValidity": "1-year",
}
request({
url: "http://t.ipoi.bid/v1/create",
method: "POST",
json: true,
headers: {
"Token": "s********************9",
"content-type": "application/json",
},
body: sendData
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body.ShortUrl);
};
});
});
return result;
}

async function getTwitterRes(data) {
const result = new Promise(resolve => {
const url = 'https://api.twitter.com/1.1/statuses/user_timeline.json?since_id=' + data.since_id + '&user_id=' + data.user_id;
oauth.get(
url,
'user token', //test user token
'user secret', //test user secret
function (e, data, res){
if (e) console.error(e);
resolve(JSON.parse(data));
}
);
})
return result;
}

async function getFileData(remotePath) {
let result = new Promise(res=>{
cos.getObject({
Bucket: 's**********82', /* 必须 */
Region: 'ap-hongkong', /* 必须 */
Key: remotePath, /* 必须 */
}, function(err, data) {
// console.log(err || data);
res(data);
});
});
return result;
}
async function saveFileData(remotePath, data) {
let result = new Promise(res=>{
cos.putObject({
Bucket: 's***********82',
Region: 'ap-hongkong',
Key: remotePath, // 这里传入前缀
Body: JSON.stringify(data),
}, function (err, data) {
res(data);
console.log(err || data);
});
});
return result;
}