This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
================================================
FILE: README.MD
================================================
# DCNews
该工具能够即时捕捉微信群中的聊天消息,并将其自动同步到预设的Discord频道中
## 功能特性
* 文本/图片/接龙 消息实时同步至discord
* 支持一同步多或者多同步一
* PIN 功能 (开发中)
* DC斜杆命令配置同步项
## 安装部署
> 实现过程:当收到一条新的群消息后,查询数据库对应表项,获取消息同步至哪个 DC 频道并发送消息,所以要配置 config.json 文件,包括 dc 机器人 key 和数据库连接方式,导入表结构,配置好表,程序即可开始同步
### docker-compose 部署
1. 配置 docker-config.json 文件, 修改数据库密码,添加 Discord 机器人授权 token, 对应下面docker-compose.yaml文件
```go
{
"Discord_bot_auth": "",
"Static_path": "/app/static/",
"Mysql_host":"192.168.210.11",
"Mysql_port":"3306",
"Mysql_db":"dcnews",
"Mysql_user":"root",
"Mysql_password":"root",
"Dc_createsync_prompts": "正在建立微信与Dc同步渠道...\nStep 1.请添加微信: \nStep 2.将该微信拉入目标微信群,等待30秒\nStep 3.在目标微信群输入同步码: "
}
```
2. 在 docker-compose.yaml 中修改数据库映射端口和密码
```go
mysql:
image: mysql:5.7
networks:
dcnews_network:
ipv4_address:
192.168.210.11
ports:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dcnews
```
3. 启动docker compose
```
docker compose up
```
4. 在日志中找到微信登录链接
5. 手动连接数据库,关联群组对应频道,即可开始同步
```go
create_time 添加时间
dc_user dc用户名称
wx_user 微信用户名称
wx_group 微信群组名称 必填
dc_channel_id dc频道ID 必填
dc_channel_info dc频道介绍 必填,需为URL
remark 备注
```
### 手动编译部署
1. 下载依赖包:
```shell
go mod download
```
2. 配置文件
```go
// 打开 config.json 配置 Discord 机器人key,静态文件存放路径,数据库连接方式
{
"Discord_bot_auth": "",
"Static_path": "",
"Mysql_host":"",
"Mysql_port":"",
"Mysql_db":"",
"Mysql_user":"",
"Mysql_password":"",
"Dc_createsync_prompts": "正在建立微信与Dc同步渠道...\nStep 1.请添加微信 \nStep 2.将该微信拉入目标微信群,等待30秒\nStep 3.在目标微信群输入同步码: "
}
```
3. 导入表结构
```
mysql -u -p
source dc_wx_association_table.sql
```
4. 构建项目:
```shell
go build
```
5. 运行项目:
```
chmod 744 ./dcnews
./dcnews
```
6. 项目日志:
```shell
logfile.log
```
7. 通过数据库关联群组对应频道
```
create_time 添加时间
dc_user dc用户名称
wx_user 微信用户名称
wx_group 微信群组名称 必填
dc_channel_id dc频道ID 必填
dc_channel_info dc频道介绍 必填,需为URL
remark 备注
```
## 使用DC斜杆命令创建同步
1. 在Dc中输入 /createsync 获取同步码
2. 在需要同步的微信群发送同步码,即可建立同步
================================================
FILE: config.json
================================================
{
"Discord_bot_auth": "",
"Static_path": "",
"Mysql_host":"",
"Mysql_port":"",
"Mysql_db":"",
"Mysql_user":"",
"Mysql_password":"",
"Dc_createsync_prompts": "正在建立微信与Dc同步渠道...\nStep 1.请添加微信 \nStep 2.将该微信拉入目标微信群,等待30秒\nStep 3.在目标微信群输入同步码: "
}
================================================
FILE: database.go
================================================
package main
import (
"database/sql"
"encoding/json"
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Conf_connect_db struct {
Mysql_host string
Mysql_port string
Mysql_db string
Mysql_user string
Mysql_password string
}
type Wechat_chat_log struct {
Time string
Send_user string
Send_content string
Send_group string
}
type DCNews_info struct {
dc_channel_id string
dc_channel_info string
}
func init_db() {
// 打开文件
config_file, _ := os.Open("config.json")
// 关闭文件
defer config_file.Close()
//NewDecoder创建一个从file读取并解码json对象的*Decoder,解码器有自己的缓冲,并可能超前读取部分json数据。
decoder := json.NewDecoder(config_file)
conf := Conf_connect_db{}
//Decode从输入流读取下一个json编码值并保存在v指向的值里
decoder.Decode(&conf)
database_connect_str := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", conf.Mysql_user, conf.Mysql_password, conf.Mysql_host, conf.Mysql_port, conf.Mysql_db)
db, _ = sql.Open("mysql", database_connect_str)
//设置数据库最大连接数
db.SetConnMaxLifetime(1024)
//设置上数据库最大闲置连接数
db.SetMaxIdleConns(64)
//验证连接
if err := db.Ping(); err != nil {
fmt.Println("open database fail")
return
}
}
func insert_wechat_chat_log(wechat_chat_log Wechat_chat_log) bool {
//准备sql语句
insert_sql := fmt.Sprintf("INSERT INTO `wechat_chat_log`(`time`, `send_user`, `send_content`, `send_group`) VALUES (\"%s\", \"%s\", \"%s\", \"%s\");", wechat_chat_log.Time, wechat_chat_log.Send_user, wechat_chat_log.Send_content, wechat_chat_log.Send_group)
_, err := db.Exec(insert_sql)
if err != nil {
fmt.Println("Failed to execute SQL statement:", err)
return false
}
return true
}
// 对dcnews进行判断,存在则返回channel id,否则返回 err
func judge_dcnews_state(sendgr string) (DCNews_info, error) {
var DCNews_info DCNews_info
select_sql := "select dc_channel_id, dc_channel_info from dc_wx_association_table where wx_group = ?"
err := db.QueryRow(select_sql, sendgr).Scan(&DCNews_info.dc_channel_id, &DCNews_info.dc_channel_info)
if err != nil {
fmt.Println("Failed to execute SQL statement:", err)
return DCNews_info, err
}
return DCNews_info, nil
}
// 插入同步配置
func insert_sync_dcCommandTarget(wx_group string, dc_channel_id string, dc_channel_info string) bool {
insert_sql := "INSERT INTO `dc_wx_association_table` (`create_time`, `dc_user`, `wx_user`, `wx_group`, `dc_channel_id`, `dc_channel_info`, `remark`) VALUES (NULL, NULL, NULL, ?, ?, ?, '程序自动添加');"
_, err := db.Exec(insert_sql, wx_group, dc_channel_id, dc_channel_info)
if err != nil {
// 记录日志或返回错误
fmt.Println("Failed to execute SQL statement:", err)
return false
}
return true
}
================================================
FILE: dc_wx_association_table.sql
================================================
/*
Navicat MySQL Data Transfer
Source Server : dcnews
Source Server Type : MySQL
Source Server Version : 50636
Source Host : 8.8.8.8:3306
Source Schema : dcnews
Target Server Type : MySQL
Target Server Version : 50636
File Encoding : 65001
Date: 15/08/2023 15:04:24
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for dc_wx_association_table
-- ----------------------------
DROP TABLE IF EXISTS `dc_wx_association_table`;
CREATE TABLE `dc_wx_association_table` (
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '添加时间',
`dc_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'dc用户名称',
`wx_user` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信用户名称',
`wx_group` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信群组名称',
`dc_channel_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'dc频道ID',
`dc_channel_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'dc频道介绍',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
================================================
FILE: discord.go
================================================
package main
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"log"
"os"
"github.com/bwmarrin/discordgo"
)
var s *discordgo.Session
type Configuration struct {
Discord_bot_auth string
Dc_createsync_prompts string
}
type AtSync_info struct {
dc_channel_id string
dc_channel_info string
dc_creator_name string
wx_group_name string
hashString string
}
var AtSync_msg AtSync_info
func init_dc() {
// 打开文件
config_file, err := os.Open("config.json")
if err != nil {
log.Println("Failed to open config file : ", err)
return
}
// 关闭文件
defer config_file.Close()
//NewDecoder创建一个从file读取并解码json对象的*Decoder,解码器有自己的缓冲,并可能超前读取部分json数据。
decoder := json.NewDecoder(config_file)
conf := Configuration{}
//Decode从输入流读取下一个json编码值并保存在v指向的值里
decoder.Decode(&conf)
s, err = discordgo.New("Bot " + conf.Discord_bot_auth)
if err != nil {
log.Println("Failed to create Discord session: ", err)
return
}
// 注册斜杆命令
var (
commands = []*discordgo.ApplicationCommand{
{
Name: "createsync",
Description: "创建微信到当前频道的同步",
},
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"createsync": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
// 取当前频道ID的Sha1值[:6]
hasher := sha1.New()
hasher.Write([]byte(i.ChannelID))
hash := hasher.Sum(nil)
hashString := hex.EncodeToString(hash)[:6]
AtSync_msg.dc_channel_id = i.ChannelID
AtSync_msg.dc_channel_info = "https://discord.com/channels/" + i.GuildID + "/" + i.ChannelID
AtSync_msg.dc_creator_name = ""
AtSync_msg.wx_group_name = ""
AtSync_msg.hashString = hashString
log.Println("dc_channel_id dc_channel_info", AtSync_msg.dc_channel_id, AtSync_msg.dc_channel_info)
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: conf.Dc_createsync_prompts + hashString,
},
})
},
}
)
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
// 建立连接
err = s.Open()
if err != nil {
log.Println("Failed to open connection: ", err)
}
log.Println("注册斜杆命令...")
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
for i, v := range commands {
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", v)
if err != nil {
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
}
registeredCommands[i] = cmd
}
}
func discord_connection_check() {
// 检查discord连接是否正常,否则重新连接
if s == nil || !s.DataReady {
init_dc()
}
}
func discord_send_text(content string, dc_channel_id string) {
// 发送消息
_, err := s.ChannelMessageSend(dc_channel_id, content)
if err != nil {
log.Println("Error sending text: ", err)
}
}
func discord_send_file(content string, name string, path string, dc_channel_id string) {
file, err := os.Open(path)
if err != nil {
log.Println("Error opening image file: ", err)
return
}
// 发送消息
_, err = s.ChannelFileSendWithMessage(dc_channel_id, content, name, file)
if err != nil {
log.Println("Error sending image: ", err)
}
}
================================================
FILE: docker-compose.yaml
================================================
version: '3.8'
services:
dcnews:
build: .
networks:
dcnews_network:
ipv4_address:
192.168.210.10
privileged: true
volumes:
- ./data/static:/app/static/
depends_on:
- mysql
mysql:
image: mysql:5.7
networks:
dcnews_network:
ipv4_address:
192.168.210.11
ports:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dcnews
volumes:
- ./data/mysql-data:/var/lib/mysql
- ./dc_wx_association_table.sql:/docker-entrypoint-initdb.d/dc_wx_association_table.sql
networks:
dcnews_network:
ipam:
driver: default
config:
- subnet: 192.168.210.0/24
================================================
FILE: docker-config.json
================================================
{
"Discord_bot_auth": "",
"Static_path": "/app/static/",
"Mysql_host":"192.168.210.11",
"Mysql_port":"3306",
"Mysql_db":"dcnews",
"Mysql_user":"root",
"Mysql_password":"root",
"Dc_createsync_prompts": "正在建立微信与Dc同步渠道...\nStep 1.请添加微信: \nStep 2.将该微信拉入目标微信群,等待30秒\nStep 3.在目标微信群输入同步码: "
}
================================================
FILE: go.mod
================================================
module dcnews
go 1.20
require (
github.com/bwmarrin/discordgo v0.27.1
github.com/eatmoreapple/openwechat v1.4.3
)
require (
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
)
================================================
FILE: go.sum
================================================
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/eatmoreapple/openwechat v1.4.3 h1:hpqR3M0c180GN5e6sfkqdTmna1+vnvohqv8LkS7MecI=
github.com/eatmoreapple/openwechat v1.4.3/go.mod h1:ZxMcq7IpVWVU9JG7ERjExnm5M8/AQ6yZTtX30K3rwRQ=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
================================================
FILE: main.go
================================================
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"regexp"
"strings"
"time"
"github.com/eatmoreapple/openwechat"
)
func main() {
// 初始化数据库连接
init_db()
// Discord 初始化
init_dc()
// 读取配置文件
config_file, _ := os.Open("config.json")
defer config_file.Close()
decoder := json.NewDecoder(config_file)
type Configuration struct {
Wechat_group_name string
Static_path string
}
conf := Configuration{}
decoder.Decode(&conf)
// 初始化日志文件
logfile_path := conf.Static_path + "logfile.log"
logfile, err := os.Create(logfile_path)
if err != nil {
log.Fatal(err)
}
defer logfile.Close()
log.SetOutput(logfile)
// 桌面模式
bot := openwechat.DefaultBot(openwechat.Desktop)
reloadStorage := openwechat.NewFileHotReloadStorage("storage.json")
defer reloadStorage.Close()
bot.PushLogin(reloadStorage, openwechat.NewRetryLoginOption())
self, err := bot.GetCurrentUser()
if err != nil {
log.Println(err)
return
}
groups, err := self.Groups()
fmt.Println(groups, err)
bot.MessageHandler = func(msg *openwechat.Message) {
fmt.Println(msg.Content)
sender, _ := msg.SenderInGroup()
sendgr, _ := msg.Sender()
sender_content := msg.Content
year, month, day := time.Now().Date()
hour, min, sec := time.Now().Hour(), time.Now().Minute(), time.Now().Second()
cur_time := fmt.Sprintf("%d-%02d-%02d-%02d-%02d-%02d", year, month, day, hour, min, sec)
// 群聊天记录转录
if msg.IsSendByGroup() && (msg.IsText() || msg.IsPicture()) {
// 微信群名是否配置在数据库中
DCNews_info, err := judge_dcnews_state(sendgr.NickName)
// 如果不在,则...
if err != nil {
// 判断dc是否执行同步命令 和 发送内容是否为同步码
if AtSync_msg.dc_channel_id != "" && sender_content == AtSync_msg.hashString {
log.Println(sendgr.NickName, AtSync_msg.dc_channel_id, AtSync_msg.dc_channel_info)
// 插入配置项
insert_sync_dcCommandTarget(sendgr.NickName, AtSync_msg.dc_channel_id, AtSync_msg.dc_channel_info)
// 清空变量
AtSync_msg = AtSync_info{}
}
return
}
// 检查discord连接是否正常,否则重新连接
discord_connection_check()
// 消息发送人
sender_name := sender.DisplayName
if sender.DisplayName == "" {
sender_name = sender.NickName
print(sender_name)
}
// 群名 emoji 表情清除
// discord 中 markdown []() 标签,不支持icon
icon_str := regexp.MustCompile(` ?[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7FF}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}\x{1FA00}-\x{1FA6F}\x{2600}-\x{26FF}\x{2700}-\x{27BF}] ?`)
sendgr_name := icon_str.ReplaceAllString(sendgr.NickName, "")
if msg.IsPicture() {
save_path := fmt.Sprintf("%s%s.jpg", conf.Static_path, cur_time)
msg.SaveFileToLocal(save_path)
discord_text_msg := fmt.Sprintf("> [%s](%s) - %s:\n", sendgr_name, DCNews_info.dc_channel_info, sender_name)
discord_send_file(discord_text_msg, cur_time+".jpg", save_path, DCNews_info.dc_channel_id)
return
}
fmt.Println(sender, err, sender_content, sendgr)
// 格式化文本
// 每行添加 >
format_content := "> " + strings.ReplaceAll(sender_content, "\n", "\n> ")
// 引用符合替换
format_content = strings.ReplaceAll(format_content, "- - - - - - - - - - - - - - -", "-----------------------------")
discord_text_msg := fmt.Sprintf("> [%s](%s) - %s:\n%s", sendgr_name, DCNews_info.dc_channel_info, sender_name, format_content)
discord_send_text(discord_text_msg, DCNews_info.dc_channel_id)
fmt.Println(*sendgr)
fmt.Println(format_content)
fmt.Println(sender_name, sender.NickName, sender.RemarkName, sender.DisplayName)
}
}
bot.Block()
// 关闭 Discord 连接
s.Close()
}