为typecho博客添加Gotify插件通知
2025-08-11 11:10:31 # 技术笔记

受限于typecho博客没有通知,自己写了一个博客有评论就通知的gotify插件,脚本使用之前的python改的。这是效果

下载地址

插件+app+docker部署

https://wonder1999.lanzouu.com/iB6DK32mdwcf

代码

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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302

<?php

namespace TypechoPlugin\GotifyNotify;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
use Widget\Options;
use Typecho\Plugin\Exception as PluginException;
use Typecho\Widget\Exception as WidgetException;
use Typecho\Db\Exception as DbException;
use Utils;

if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}

/**
* 当用户评论时通过 Gotify 发送通知
*
* @package GotifyNotify
* @author 白荼
* @version 1.3.0
* @link https://gotify.net
*/
class Plugin implements PluginInterface
{
/**
* 激活插件方法
*
* @access public
* @return void
* @throws PluginException
*/
public static function activate()
{
\Typecho\Plugin::factory('Widget_Feedback')->finishComment = __CLASS__ . '::requestService';
\Typecho\Plugin::factory('Widget_Comments_Edit')->finishComment = __CLASS__ . '::requestService';
\Typecho\Plugin::factory('Widget_Service')->sendGotify = __CLASS__ . '::sendGotify';
}

/**
* 禁用插件方法
*
* @access public
* @return void
* @throws PluginException
*/
public static function deactivate()
{
// 通常不需要显式移除
}

/**
* 获取插件配置面板
*
* @param Form $form 配置面板
* @access public
* @return void
*/
public static function config(Form $form)
{
// Gotify 服务器地址
$serverUrl = new Text('serverUrl', NULL, '', _t('Gotify 服务器地址'), _t('例如: http://your-gotify-server.com'));
$form->addInput($serverUrl->addRule('required', _t('Gotify 服务器地址不能为空')));

// 应用 Token
$appToken = new Text('appToken', NULL, '', _t('应用 Token'), _t('在 Gotify 中创建应用后获得的 Token'));
$form->addInput($appToken->addRule('required', _t('应用 Token 不能为空')));

// 通知标题
$title = new Text('title', NULL, '博客有新评论', _t('通知标题'), _t('收到新评论时的推送标题'));
$form->addInput($title);

// 消息优先级 (Priority) - 使用 Text 输入框
$priority = new Text('priority', NULL, '1', _t('消息优先级'), _t('设置 Gotify 消息的优先级 (自己在Gorify定义的Priority)'));
$form->addInput($priority);
}

/**
* 个人用户的配置面板
*
* @param Form $form
* @access public
* @return void
*/
public static function personalConfig(Form $form)
{
// 个人配置通常为空
}

/**
* 评论通知回调 - 触发异步服务
* 这个方法在评论完成后被调用
*
* @access public
* @param $comment (Widget_Comments_Edit 或 Widget_Feedback 的实例)
* @return void
* @throws PluginException
*/
public static function requestService($comment)
{
// 检查评论对象是否有效
if (!isset($comment->coid) || !$comment->have()) {
error_log("GotifyNotify: Invalid comment object received in requestService.");
return;
}

$coid = $comment->coid;
$options = Options::alloc()->plugin('GotifyNotify');


// 检查必要配置
if (empty($options->serverUrl) || empty($options->appToken)) {
error_log("GotifyNotify: Missing server URL or app token, skipping notification for comment ID {$coid}.");
return;
}

// 调用 Widget_Service 中注册的异步方法
// 将评论 ID 传递给异步服务
try {
error_log("GotifyNotify: Calling async service for comment ID {$coid}.");
Utils\Helper::requestService('sendGotify', $coid);
} catch (\Exception $e) {
error_log("GotifyNotify: Failed to call async service for comment ID {$coid}. Error: " . $e->getMessage());
}
}

/**
* 异步发送 Gotify 通知
* 这个方法由 Widget_Service 调用
*
* @param integer $coid 评论ID
* @access public
* @return void
* @throws WidgetException
* @throws DbException
*/
public static function sendGotify(int $coid)
{
error_log("GotifyNotify Service: Starting to process comment ID {$coid}.");

// 获取插件配置
$options = Options::alloc()->plugin('GotifyNotify');

// 移除了对 $options->enable 的检查,插件默认启用

// 重新获取评论对象
try {
$commentWidget = Utils\Helper::widgetById('comments', $coid);
} catch (WidgetException $e) {
error_log("GotifyNotify Service: Failed to get comment widget for ID {$coid}. Error: " . $e->getMessage());
return;
}

if (!$commentWidget->have()) {
error_log("GotifyNotify Service: Comment widget is empty for ID {$coid}.");
return;
}

// 检查必要配置
if (empty($options->serverUrl) || empty($options->appToken)) {
error_log("GotifyNotify Service: Missing configuration for comment ID {$coid}.");
return;
}

try {
// 构造通知内容
$title = $options->title ?: '博客有新评论';
$message = self::buildMessage($commentWidget); // 传入 widget 对象
// 获取优先级设置
$priority = $options->priority ?? '1'; // 默认优先级为 1

error_log("GotifyNotify Service: Prepared message for comment ID {$coid}. Title: {$title}, Priority: {$priority}");

// 发送通知,传入优先级
self::sendGotifyMessage($options->serverUrl, $options->appToken, $title, $message, $priority);
error_log("GotifyNotify Service: Successfully sent notification for comment ID {$coid}.");

} catch (\Exception $e) {
// 记录详细错误信息
error_log("GotifyNotify Service: Failed to send notification for comment ID {$coid}. Error: " . $e->getMessage());
error_log("GotifyNotify Service: Stack trace: " . $e->getTraceAsString());
}
}

/**
* 构造通知消息内容
*
* @param \Widget\Base\Comments $comment 评论对象
* @return string 消息内容
*/
private static function buildMessage($comment)
{
$content = '';

if (is_object($comment) && $comment->have()) {
// 从评论对象获取信息
$author = $comment->author ?? '匿名用户';
$mail = $comment->mail ?? '';
$url = $comment->url ?? '';
$text = $comment->text ?? '';
// $ip = $comment->ip ?? ''; // Widget\Base\Comments 通常不直接暴露 IP

$content = "作者: {$author}\n";
if (!empty($mail)) {
$content .= "邮箱: {$mail}\n";
}
if (!empty($url)) {
$content .= "网站: {$url}\n";
}
// if (!empty($ip)) {
// $content .= "IP: {$ip}\n";
// }
$content .= "内容: {$text}\n";
$content .= "时间: " . date('Y-m-d H:i:s', $comment->created) . "\n"; // 使用评论创建时间
$content .= "文章: " . $comment->title . "\n"; // 文章标题
$content .= "链接: " . $comment->permalink . "\n"; // 评论链接
} else {
// 兼容其他情况
$content = "收到新评论,请登录后台查看 (评论ID: " . ($comment->coid ?? 'unknown') . ")";
}

return $content;
}

/**
* 发送 Gotify 消息
*
* @param string $serverUrl Gotify 服务器地址
* @param string $appToken 应用 Token
* @param string $title 消息标题
* @param string $message 消息内容
* @param string $priority 消息优先级
* @throws \Exception
*/
private static function sendGotifyMessage($serverUrl, $appToken, $title, $message, $priority = '1')
{
// 清理 URL
$serverUrl = rtrim($serverUrl, '/');

// 构造请求 URL (使用查询参数传递 token)
$url = $serverUrl . '/message';

// 准备表单数据,包含 priority
$data = [
'title' => $title,
'message' => $message,
'priority' => $priority // 使用传入的优先级
];

// 准备查询参数
$params = ['token' => $appToken];
$fullUrl = $url . '?' . http_build_query($params);

error_log("GotifyNotify: Sending request to {$fullUrl}");
error_log("GotifyNotify: POST data: " . print_r($data, true));

// 使用 cURL 发送请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // 发送表单数据
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 根据你的服务器 SSL 配置调整
curl_setopt($ch, CURLOPT_USERAGENT, 'Typecho GotifyNotify Plugin/1.3.0');

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

error_log("GotifyNotify: cURL Response Code: {$httpCode}");
error_log("GotifyNotify: cURL Response Body: {$response}");
error_log("GotifyNotify: cURL Error (if any): {$error}");

// 检查响应
if ($response === false) {
throw new \Exception('cURL error: ' . $error);
}

if ($httpCode >= 200 && $httpCode < 300) {
// 成功
error_log("GotifyNotify: Message sent successfully.");
// 可以进一步检查返回的 JSON
$result = json_decode($response, true);
if (!$result) {
error_log("GotifyNotify: Warning - Could not decode JSON response, but HTTP status was OK.");
}
return; // 成功返回
} else {
// 失败
throw new \Exception('HTTP error: ' . $httpCode . ', response: ' . $response);
}
}
}