Compare commits

...

10 Commits

13 changed files with 218 additions and 42 deletions

View File

@@ -103,6 +103,7 @@ inputs:
mariadb.mountFrom = "nodatacow";
open-webui.ollamaHost = "127.0.0.1";
howdy = {};
postgresql.enable = true;
};
bugs = [ "amdpstate" ];
packages = { mathematica = {}; vasp = {}; };

View File

@@ -5,10 +5,10 @@ coturn:
tinc: ENC[AES256_GCM,data:E3OrPA67R48x5FJUW0ZbERlclz8Z/XokAaGTeBQLPEHSeqEArHYSZkdJRZejFrBruJPlGZMPNBQzlIBXOfXKwMnlBDaGJIIJHIzPDGG9W7QF4IIRK/BjVZHFwfKvZtbUDGsqLcCSe5+ttmyucBaFGquXhnD/Tu09uyWtRvS10KAJLY0Z2/16CFB1+8egJIcYw2TFXObo+KR92Va0qwiDSepKaJtYLimDGRKk04QGj+BYa5y8PjIG6bz8UG82mmCiV7XM3EPlSMA=,iv:kawsklNGFbRhxKuUwvNL2WyBxuYu2T/uks1cJ4i8NhA=,tag:V+jAaxQX7JCiR5+wIVW4Nw==,type:str]
postgresql:
headscale: ENC[AES256_GCM,data:z2cyyT1TcIhNJCBeGn072aFI2nAioWZQvpyzoky4tWtMymKlw4ilOtSYAsp+kaNOoqvWSmoAQNJLNzeDk1iTCQ==,iv:hZdS/CAVBO0k/AmX3qw3YwTYgK49Aeu5QI3YCAduiZ0=,tag:2l4GPV/T2GHjAAUDX3LaEA==,type:str]
missgram: ENC[AES256_GCM,data:zUY6397ThfeHDD8/Msy3mWnTjXCkEhpgsUwcjXnhIiNET1J14hIojCbwUpdCtGTFF+RQOtaS9aGSp8ctQeWIwg==,iv:0+WeCoMFQFhnzjSfvz0ZnqK6FIn1QBHr9fB+tjBNSDk=,tag:Bf5krX2hxIPlkdiAXppSqA==,type:str]
missgram:
secret: ENC[AES256_GCM,data:qsxJue8mGAJejSxOoPd4MXD06upnk0fxUM5EKPBs/WI=,iv:HaHj/vJkIERUQ0Lr93s9kaApNWPjDcpLu2897qmCjqA=,tag:u73jUDd6pGKk1yir/oF4hQ==,type:str]
telegramBotToken: ENC[AES256_GCM,data:kNGhj1SjyK1H8NJmJLi90cpGtWmmGpFEFFT/JkDX4QqxbOC6BfFIMgzVsZ5GVQ==,iv:sccRmCs8HBAvi9mDAaz8OjxqXLAVXepJHaj7RrUt6kI=,tag:RuK3EdRMVhS9pVDw50lW6A==,type:str]
telegramChatId: ENC[AES256_GCM,data:Wy5PWg2nJgeL3zMquEk=,iv:FU9wl2eJzCH88lMLqRW6WX66h43Iw/jrdWsPwFbi7+Y=,tag:8OON6H1R6NWa+RqA/KxIrA==,type:str]
sops:
age:
- recipient: age19ax6vm3pv8rph5tq3mmehd9sy9jk823tw8svsd790r0lkslycquqvlwz9m
@@ -29,7 +29,7 @@ sops:
ZXFTU3ZCaW1pTVh0RUJzdDdGdHlPYTgK2mlgcX2kEc8+2UDdBnhUm6IIuh8V6agW
ooxH9OEPXUVI/4JcDo4v8ZUhAyU1ehLH0Ef7PJCChOZe2KZmWSNbhA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-12-27T03:38:11Z"
mac: ENC[AES256_GCM,data:/IM0SIc9BaGaVl3k5173R6Zz/Z87hexrAL4y0TGMNyvMy2ZrG2XJvr01XW+YbE9TPQxNZQDW9e6Xfn2jKoz+EUPEjSVEr2XC12ZUhwnEu9x99lmwvUrf8CwuXAfUIUN4hJvv7e5PsjaVkR1VAs/t6d5gYlqgX45oi8WMX6JQl/U=,iv:e8nzV4S/8F/5jcYlPwHyBBffutULS3kYOaIApv9MTBA=,tag:gt3g2pBPgfKv80kGYSb87A==,type:str]
lastmodified: "2025-12-28T09:13:01Z"
mac: ENC[AES256_GCM,data:rVjqBfoA/DUgb1Yqc3FzeMBPWJniAYKYcbLauh5flpKYfcTp01lr/pTbyB5BLEHZLOYwMf2PNjfG8zPDKv2z1cjYwoYEj9bur4N6pagR/NFuAoEvgOjm0YlrTVkskRmLxaqxYB749y4wWS04MvJhfPON4hWMdoYguPBmCpZMKqc=,iv:4ZVGsLKQNxqKmpaDcIpA21rAe51TKVR8diN5/d7SOQg=,tag:pIw/tqw+211V/0xK9M3hvg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0

View File

@@ -14,7 +14,8 @@ lib: rec
(
let handle = module: let type = builtins.typeOf module; in
if type == "path" || type == "string" then (handle (import module))
else if type == "lambda" then ({ pkgs, utils, ... }@inputs: (module inputs))
else if type == "lambda" && builtins.functionArgs module == {}
then ({ pkgs, utils, ... }@inputs: (module inputs))
else module;
in handle
)

View File

@@ -181,13 +181,8 @@ inputs:
"update.mode" = "none";
"editor.tabSize" = 2;
"nix.enableLanguageServer" = true;
"nix.serverPath" = "nil";
"nix.serverPath" = "nixd";
"nix.formatterPath" = "nixpkgs-fmt";
"nix.serverSettings"."nil" =
{
"diagnostics"."ignored" = [ "unused_binding" "unused_with" ];
"formatting"."command" = [ "nixpkgs-fmt" ];
};
"xmake.envBehaviour" = "erase";
"git.openRepositoryInParentFolders" = "never";
"todo-tree.regex.regex" = "(//|#|<!--|;|/\\*|^|%|^[ \\t]*(-|\\d+.))\\s*($TAGS)";

View File

@@ -29,7 +29,11 @@ inputs:
};
nixos =
{
services.nginx.https."missgram.chn.moe".location."/".proxy.upstream = "http://127.0.0.1:9173";
services =
{
nginx.https."missgram.chn.moe".location."/".proxy.upstream = "http://127.0.0.1:9173";
postgresql.instances.missgram = {};
};
system.sops =
{
templates."missgram/config.yml" =
@@ -41,12 +45,12 @@ inputs:
{
Secret = placeholder."missgram/secret";
TelegramBotToken = placeholder."missgram/telegramBotToken";
TelegramChatId = placeholder."missgram/telegramChatId";
TelegramChatId = -1003641252872;
ServerPort = 9173;
dbPassword = placeholder."postgresql/missgram";
};
};
secrets = inputs.lib.genAttrs' [ "secret" "telegramBotToken" "telegramChatId" ]
(n: inputs.lib.nameValuePair "missgram/${n}" {});
secrets = { "missgram/secret" = {}; "missgram/telegramBotToken" = {}; };
};
};
};

View File

@@ -144,7 +144,7 @@ inputs: rec
buildProxy = inputs.pkgs.lib.mkBuildproxy ./pybinding/proxy.nix;
};
brokenaxes = inputs.pkgs.python3Packages.callPackage ./brokenaxes.nix { src = inputs.topInputs.brokenaxes; };
missgram = inputs.pkgs.callPackage ./missgram { inherit biu; stdenv = inputs.pkgs.clang18Stdenv; };
missgram = inputs.pkgs.callPackage ./missgram { inherit biu sqlgen; stdenv = inputs.pkgs.clang18Stdenv; };
sqlgen = inputs.pkgs.callPackage ./sqlgen.nix { src = inputs.topInputs.sqlgen; inherit reflectcpp; };
reflectcpp = inputs.pkgs.callPackage ./reflectcpp.nix { src = inputs.topInputs.reflectcpp; };

View File

@@ -11,13 +11,16 @@ endif()
find_package(biu REQUIRED)
find_package(httplib REQUIRED)
find_package(sqlgen REQUIRED)
add_executable(missgram src/main.cpp)
target_link_libraries(missgram PRIVATE biu::biu httplib::httplib)
add_executable(missgram src/main.cpp src/db.cpp src/tg.cpp)
target_include_directories(missgram PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_link_libraries(missgram PRIVATE biu::biu httplib::httplib sqlgen::sqlgen)
target_compile_features(missgram PRIVATE cxx_std_23)
if(DEFINED MISSGRAM_CONFIG_FILE)
target_compile_definitions(missgram PRIVATE MISSGRAM_CONFIG_FILE="${MISSGRAM_CONFIG_FILE}")
endif()
target_compile_definitions(missgram PRIVATE BIU_LOGGER_SOURCE_ROOT="${CMAKE_CURRENT_SOURCE_DIR}")
install(TARGETS missgram RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
get_property(ImportedTargets DIRECTORY "${CMAKE_SOURCE_DIR}" PROPERTY IMPORTED_TARGETS)

View File

@@ -1,8 +1,8 @@
{ lib, stdenv, cmake, pkg-config, biu, configFile ? null, httplib }: stdenv.mkDerivation
{ lib, stdenv, cmake, pkg-config, biu, configFile ? null, httplib, sqlgen }: stdenv.mkDerivation
{
name = "missgram";
src = ./.;
buildInputs = [ biu httplib ];
buildInputs = [ biu httplib sqlgen ];
nativeBuildInputs = [ cmake pkg-config ];
cmakeFlags = lib.optional (configFile != null) [ "-DMISSGRAM_CONFIG_FILE=${configFile}" ];
}

View File

@@ -0,0 +1,19 @@
# include <biu.hpp>
namespace missgram
{
struct Config
{
std::string Secret;
std::string TelegramBotToken;
std::int64_t TelegramChatId;
std::int16_t ServerPort;
std::string dbPassword;
} inline config;
struct File { std::string url; bool is_photo; bool should_hidden; };
void db_write(std::string misskey_note, std::int32_t telegram_message_id);
std::optional<std::int32_t> db_read(std::string misskey_note);
std::optional<std::int32_t> tg_send(std::string text, std::optional<std::int32_t> replyId, std::vector<File> files);
}

View File

@@ -0,0 +1,23 @@
# include <missgram.hpp>
# include <sqlgen/postgres.hpp>
struct Record { std::string misskey_note; std::int32_t telegram_message_id; };
void missgram::db_write(std::string misskey_note, std::int32_t telegram_message_id)
{
auto&& conn = sqlgen::postgres::connect
({.user = "missgram", .password = config.dbPassword, .host = "127.0.0.1", .dbname = "missgram"});
sqlgen::write(conn, Record{misskey_note, telegram_message_id});
}
std::optional<std::int32_t> missgram::db_read(std::string misskey_note)
{
using namespace sqlgen::literals;
auto&& conn = sqlgen::postgres::connect
({.user = "missgram", .password = config.dbPassword, .host = "127.0.0.1", .dbname = "missgram"});
auto query = sqlgen::read<std::vector<Record>> |
sqlgen::where("misskey_note"_c == misskey_note) |
sqlgen::limit(1);
auto result = query(conn);
if (!result || result->empty()) return {}; else return result->front().telegram_message_id;
}

View File

@@ -1,6 +1,6 @@
# include <biu.hpp>
# include <missgram.hpp>
# include <httplib.h>
# include <tgbot/tgbot.h>
# ifndef MISSGRAM_CONFIG_FILE
# define MISSGRAM_CONFIG_FILE "./config.yaml"
# endif
@@ -8,15 +8,10 @@
int main()
{
using namespace biu::literals;
using namespace missgram;
biu::Logger::Guard log;
struct Config
{
std::string Secret;
std::string TelegramBotToken;
std::string TelegramChatId;
int ServerPort;
} config = YAML::LoadFile(MISSGRAM_CONFIG_FILE).as<Config>();
config = YAML::LoadFile(MISSGRAM_CONFIG_FILE).as<Config>();
biu::Logger::try_exec([&]
{
@@ -26,44 +21,90 @@ int main()
{
biu::Logger::try_exec([&]
{
log.debug(req.body);
log.debug("{}"_f(req.headers));
if (req.get_header_value("x-misskey-hook-secret") != config.Secret)
throw std::runtime_error("Invalid secret key.");
struct Content
{
std::string type, server;
struct
struct Body
{
struct Note
{
std::string text, visibility;
std::optional<std::string> replyId;
std::string id, visibility;
std::optional<std::string> text, replyId;
struct Renote { std::string id; };
std::optional<Renote> renote;
bool localOnly;
struct File { bool isSensitive; std::string url; std::string type; };
std::vector<File> files;
};
std::optional<Note> note;
} body;
};
auto content = YAML::Load(req.body).as<Content>();
log();
// 只考虑公开且允许联合的帖子。
if
(
content.type != "note" // 只转发 note 的情况
content.type != "note" // 只考虑 note 的情况这里note包括了回复、转发、引用
|| !content.body.note // 大概不会发生,但还是判断一下
|| content.body.note->visibility != "public" // 只转发公开的 note
|| content.body.note->replyId // 不转发回复
|| content.body.note->visibility != "public" || content.body.note->localOnly // 只转发公开的、允许联合的帖子
) return;
std::string text = content.body.note->text;
if (content.body.note->renote)
text += "\n🔁 Renote: {}/notes/{}"_f(content.server, content.body.note->renote->id);
TgBot::Bot bot(config.TelegramBotToken);
// bot.getApi().sendMessage(config.TelegramChatId, text);
// 接下来准备要转发的文字内容
std::string text;
std::optional<std::uint32_t> reply_id;
// 如果是转发,则直接写链接
if (!content.body.note->text)
text = "转发了[帖子]({}/notes/{})"_f(content.server, content.body.note->id);
// 否则(引用或普通帖子)
else
{
text = *content.body.note->text;
// 如果有引用,则需要查找被引用的帖子是否已经被转发过,若是则直接回复被转发的消息。
// 如果没有被转发过,则在开头附上链接
if (content.body.note->renote)
{
reply_id = db_read(content.body.note->renote->id);
if (!reply_id)
text = "引用了[帖子]({}/notes/{})\n"_f(content.server, content.body.note->renote->id) + text;
}
// 检查是否是回复帖子,若是则在开头附上链接原帖链接。我一般不直接回复自己的帖子,所以这里不检查
if (content.body.note->replyId)
text = "回复了[帖子]({}/notes/{})\n"_f(content.server, *content.body.note->replyId) + text;
// 最后附上原贴地址
text += "\n[在联邦宇宙查看]({}/notes/{})"_f(content.server, content.body.note->id);
}
// 接下来整理要转发的文件
auto files = content.body.note->files | ranges::views::transform([](auto&& file) -> File
{
return File
{
.url = file.url,
.is_photo = file.type.starts_with("image/"),
.should_hidden = file.isSensitive
};
}) | ranges::to_vector;
log();
// 异步发送消息
std::thread([text, note_id = content.body.note->id, reply_id, files]
{
auto message_id = tg_send(text, reply_id, files);
if (message_id) db_write(note_id, *message_id);
}).detach();
// 完成 http 响应
res.status = 200;
res.body = "OK";
log.debug(req.body);
});
});
svr.listen("0.0.0.0", config.ServerPort);

View File

@@ -0,0 +1,89 @@
# include <missgram.hpp>
# include <tgbot/tgbot.h>
std::optional<std::int32_t> missgram::tg_send
(std::string text, std::optional<std::int32_t> replyId, std::vector<File> files)
{
using namespace biu::literals;
// 整理要发送的信息
TgBot::Bot bot(config.TelegramBotToken);
std::shared_ptr<TgBot::ReplyParameters> reply;
if (replyId) reply = std::make_shared<TgBot::ReplyParameters>(*replyId, config.TelegramChatId);
auto attachs = files
| ranges::views::transform([&](auto&& file) -> TgBot::InputMedia::Ptr
{
if (file.is_photo)
{
auto pic = std::make_shared<TgBot::InputMediaPhoto>();
pic->media = file.url;
pic->hasSpoiler = file.should_hidden;
return pic;
}
else
{
auto doc = std::make_shared<TgBot::InputMediaDocument>();
doc->media = file.url;
return doc;
}
})
| ranges::to_vector;
// 多次尝试运行函数直到成功或达到最大尝试次数5次
auto try_run = [&](auto&& func) -> std::optional<std::int32_t>
{
auto retry_delay = 1s;
int attempts = 0;
while (attempts < 5)
{
TgBot::Message::Ptr message;
biu::Logger::try_exec([&] { message = func(); });
if (message) return message->messageId;
std::this_thread::sleep_for(retry_delay);
retry_delay *= 2;
attempts++;
}
return std::nullopt;
};
// 如果没有附件,使用 sendMessage 发送文本消息
if (attachs.empty()) return try_run([&] { return bot.getApi().sendMessage
(
config.TelegramChatId, text, nullptr, reply, nullptr,
"MarkdownV2"
);});
// 如果只有一个附件并且是图片,使用 sendPhoto 发送
else if (attachs.size() == 1 && files[0].is_photo) return try_run([&]
{
return bot.getApi().sendPhoto
(
config.TelegramChatId, files[0].url, text, reply,
nullptr, "MarkdownV2", false, {}, 0, false, files[0].should_hidden
);
});
// 如果有多个附件,使用 sendMediaGroup 分两条消息发送,返回第一条的 id
else
{
auto message = try_run([&] { return bot.getApi().sendMessage
(
config.TelegramChatId, text, nullptr, reply, nullptr,
"MarkdownV2"
);});
if (message)
{
auto message2 = try_run([&] -> TgBot::Message::Ptr
{
auto msg = bot.getApi().sendMediaGroup
(
config.TelegramChatId, attachs, false,
std::make_shared<TgBot::ReplyParameters>(*message, config.TelegramChatId)
);
if (msg.empty() || !ranges::all_of(msg, [](auto&& m) { return bool(m); }))
return nullptr;
else return msg[0];
});
if (!message2) return {};
}
return message;
}
}

View File

@@ -3,6 +3,6 @@
name = "sqlgen";
inherit src;
nativeBuildInputs = [ cmake pkg-config ];
buildInputs = [ postgresql reflectcpp ];
propagatedBuildInputs = [ postgresql reflectcpp ];
cmakeFlags = [ "-DSQLGEN_USE_VCPKG=OFF" "-DSQLGEN_SQLITE3=OFF" "-DBUILD_SHARED_LIBS=ON" ];
}