feat: only retrieve 50 download records
This commit is contained in:
1 parent
24d0776fee
commit
de5b261666
9 files changed
+259
-87
No files matched your search
@@ -69,7 +69,7 @@ CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 SDKROOT=$(xcrun --sdk macosx --show-sdk-p
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
# CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o wx_video_download
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o wx_video_download
|
||||
|
||||
$env:CGO_ENABLED=0; $env:GOOS="linux"; $env:GOARCH="amd64"; go build -trimpath -ldflags="-s -w" -o wx_video_download
|
||||
|
||||
|
||||
+39
-25
@@ -52,6 +52,7 @@ func init() {
|
||||
root_cmd.AddCommand(serve_cmd)
|
||||
serve_cmd.AddCommand(server_status_cmd)
|
||||
serve_cmd.AddCommand(mp_stop_cmd)
|
||||
serve_cmd.AddCommand(mp_restart_cmd)
|
||||
}
|
||||
|
||||
func serve_command() {
|
||||
@@ -207,34 +208,47 @@ var mp_stop_cmd = &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "停止公众号服务",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
pid, err := read_wx_pidfile()
|
||||
if err != nil || pid == 0 {
|
||||
color.Red("未发现守护进程")
|
||||
return
|
||||
}
|
||||
if !system.IsProcessRunning(pid) {
|
||||
color.Green("进程已停止")
|
||||
_ = remove_wx_pidfile()
|
||||
return
|
||||
}
|
||||
_ = system.TerminateProcess(pid)
|
||||
expire := time.After(8 * time.Second)
|
||||
tick := time.NewTicker(200 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
if !system.IsProcessRunning(pid) {
|
||||
_ = remove_wx_pidfile()
|
||||
color.Green("服务已关闭")
|
||||
return
|
||||
}
|
||||
case <-expire:
|
||||
color.Red("关闭超时")
|
||||
stop_daemon()
|
||||
},
|
||||
}
|
||||
|
||||
var mp_restart_cmd = &cobra.Command{
|
||||
Use: "restart",
|
||||
Short: "重启公众号服务",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
stop_daemon()
|
||||
start_daemon()
|
||||
},
|
||||
}
|
||||
|
||||
func stop_daemon() {
|
||||
pid, err := read_wx_pidfile()
|
||||
if err != nil || pid == 0 {
|
||||
color.Red("未发现守护进程")
|
||||
return
|
||||
}
|
||||
if !system.IsProcessRunning(pid) {
|
||||
color.Green("进程已停止")
|
||||
_ = remove_wx_pidfile()
|
||||
return
|
||||
}
|
||||
_ = system.TerminateProcess(pid)
|
||||
expire := time.After(8 * time.Second)
|
||||
tick := time.NewTicker(200 * time.Millisecond)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
if !system.IsProcessRunning(pid) {
|
||||
_ = remove_wx_pidfile()
|
||||
color.Green("服务已关闭")
|
||||
return
|
||||
}
|
||||
case <-expire:
|
||||
color.Red("关闭超时")
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func port_listening(addr string) bool {
|
||||
|
||||
+59
-2
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -41,13 +42,69 @@ func NewAPIClient(cfg *APIConfig, parent_logger *zerolog.Logger) *APIClient {
|
||||
official_cfg := officialaccount.NewOfficialAccountConfig(cfg.Original, cfg.RemoteServerMode)
|
||||
officialaccount_client := officialaccount.NewOfficialAccountClient(official_cfg, parent_logger)
|
||||
channels_client = channels.NewChannelsClient()
|
||||
|
||||
get_sorted_tasks := func() []*downloadpkg.Task {
|
||||
tasks := downloader.GetTasks()
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
|
||||
})
|
||||
return tasks
|
||||
}
|
||||
|
||||
channels_client.OnConnected = func(client *channels.Client) {
|
||||
// Initial tasks
|
||||
tasks := downloader.GetTasks()
|
||||
if data, err := json.Marshal(APIClientWSMessage{Type: "tasks", Data: tasks}); err == nil {
|
||||
all_tasks := get_sorted_tasks()
|
||||
limit := 50
|
||||
if limit > len(all_tasks) {
|
||||
limit = len(all_tasks)
|
||||
}
|
||||
tasks := all_tasks[:limit]
|
||||
if data, err := json.Marshal(APIClientWSMessage{Type: "tasks", Data: map[string]interface{}{
|
||||
"list": tasks,
|
||||
"total": len(all_tasks),
|
||||
}}); err == nil {
|
||||
client.Send <- data
|
||||
}
|
||||
}
|
||||
|
||||
channels_client.OnMessage = func(client *channels.Client, message []byte) {
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if err := json.Unmarshal(message, &req); err == nil && req.Type == "fetch_tasks" {
|
||||
allTasks := get_sorted_tasks()
|
||||
start := (req.Page - 1) * req.Limit
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 50
|
||||
}
|
||||
|
||||
if start >= len(allTasks) {
|
||||
if data, err := json.Marshal(APIClientWSMessage{Type: "tasks", Data: map[string]interface{}{
|
||||
"list": []*downloadpkg.Task{},
|
||||
"total": len(allTasks),
|
||||
}}); err == nil {
|
||||
client.Send <- data
|
||||
}
|
||||
return
|
||||
}
|
||||
end := start + req.Limit
|
||||
if end > len(allTasks) {
|
||||
end = len(allTasks)
|
||||
}
|
||||
tasks := allTasks[start:end]
|
||||
if data, err := json.Marshal(APIClientWSMessage{Type: "tasks", Data: map[string]interface{}{
|
||||
"list": tasks,
|
||||
"total": len(allTasks),
|
||||
}}); err == nil {
|
||||
client.Send <- data
|
||||
}
|
||||
}
|
||||
}
|
||||
logger := parent_logger.With().Str("Client", "api_client").Logger()
|
||||
client := &APIClient{
|
||||
downloader: downloader,
|
||||
|
||||
+42
-16
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -269,10 +270,15 @@ func (c *APIClient) handleCreateFeedDownloadTask(ctx *gin.Context) {
|
||||
result.Err(ctx, 500, "创建任务失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "tasks",
|
||||
Data: c.downloader.GetTasks(),
|
||||
})
|
||||
task := c.downloader.GetTask(id)
|
||||
if task != nil {
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "event",
|
||||
Data: map[string]interface{}{
|
||||
"task": task,
|
||||
},
|
||||
})
|
||||
}
|
||||
result.Ok(ctx, gin.H{"id": id})
|
||||
}
|
||||
|
||||
@@ -328,6 +334,9 @@ func (c *APIClient) handleFetchTaskList(ctx *gin.Context) {
|
||||
filter.Statuses = []base.Status{base.Status(status)}
|
||||
}
|
||||
list := c.downloader.GetTasksByFilter(filter)
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].CreatedAt.After(list[j].CreatedAt)
|
||||
})
|
||||
total := len(list)
|
||||
page_num, err := strconv.Atoi(page_str)
|
||||
if err != nil {
|
||||
@@ -429,10 +438,15 @@ func (c *APIClient) handleCreateLiveTask(ctx *gin.Context) {
|
||||
result.Err(ctx, 500, "创建任务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "tasks",
|
||||
Data: c.downloader.GetTasks(),
|
||||
})
|
||||
task := c.downloader.GetTask(id)
|
||||
if task != nil {
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "event",
|
||||
Data: map[string]interface{}{
|
||||
"task": task,
|
||||
},
|
||||
})
|
||||
}
|
||||
result.Ok(ctx, gin.H{"id": id})
|
||||
}
|
||||
|
||||
@@ -474,10 +488,17 @@ func (c *APIClient) handleBatchCreateTask(ctx *gin.Context) {
|
||||
// Int("count", len(task.Reqs)).
|
||||
// Dur("cost", time.Since(start)).
|
||||
// Msg("批量创建任务完成")
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "tasks",
|
||||
Data: c.downloader.GetTasks(),
|
||||
})
|
||||
for _, id := range ids {
|
||||
task := c.downloader.GetTask(id)
|
||||
if task != nil {
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "event",
|
||||
Data: map[string]interface{}{
|
||||
"task": task,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
result.Ok(ctx, gin.H{"ids": ids})
|
||||
}
|
||||
|
||||
@@ -682,10 +703,15 @@ func (c *APIClient) handleCreateChannelsTask(ctx *gin.Context) {
|
||||
result.Err(ctx, 500, "下载失败")
|
||||
return
|
||||
}
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "tasks",
|
||||
Data: c.downloader.GetTasks(),
|
||||
})
|
||||
task := c.downloader.GetTask(id)
|
||||
if task != nil {
|
||||
c.channels.Broadcast(APIClientWSMessage{
|
||||
Type: "event",
|
||||
Data: map[string]interface{}{
|
||||
"task": task,
|
||||
},
|
||||
})
|
||||
}
|
||||
result.Ok(ctx, gin.H{"id": id})
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ type ChannelsClient struct {
|
||||
cache *cache.Cache
|
||||
req_seq uint64
|
||||
OnConnected func(client *Client)
|
||||
OnMessage func(client *Client, message []byte)
|
||||
}
|
||||
|
||||
func NewChannelsClient() *ChannelsClient {
|
||||
@@ -92,8 +93,13 @@ func (c *ChannelsClient) HandleChannelsWebsocket(ctx *gin.Context) {
|
||||
c.requests_mu.RUnlock()
|
||||
if ok {
|
||||
ch <- resp
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.OnMessage != nil {
|
||||
c.OnMessage(client, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *ChannelsClient) Stop() {
|
||||
|
||||
@@ -303,7 +303,7 @@ function total_speed(tasks) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
function __wx_refresh_downloader(selector, tasks) {
|
||||
function __wx_refresh_downloader(selector, tasks, total) {
|
||||
const container = document.querySelector(selector);
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
@@ -313,7 +313,8 @@ function __wx_refresh_downloader(selector, tasks) {
|
||||
|
||||
const countEl = document.getElementById("wx-dl-count");
|
||||
if (countEl) {
|
||||
countEl.innerText = list.length > 0 ? `(${list.length})` : "";
|
||||
const count = total !== undefined ? total : list.length;
|
||||
countEl.innerText = count > 0 ? `(${count})` : "";
|
||||
}
|
||||
|
||||
if (list.length === 0) {
|
||||
@@ -470,12 +471,12 @@ function __wx_refresh_downloader(selector, tasks) {
|
||||
});
|
||||
|
||||
if (list.length > 0) {
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "weui-loadmore weui-loadmore_line";
|
||||
footer.style.marginTop = "20px";
|
||||
footer.innerHTML =
|
||||
'<span class="weui-loadmore__tips">没有更多内容了</span>';
|
||||
container.appendChild(footer);
|
||||
// const footer = document.createElement("div");
|
||||
// footer.className = "weui-loadmore weui-loadmore_line";
|
||||
// footer.style.marginTop = "20px";
|
||||
// footer.innerHTML =
|
||||
// '<span class="weui-loadmore__tips">没有更多内容了</span>';
|
||||
// container.appendChild(footer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ var ua = navigator.userAgent || navigator.platform || "";
|
||||
var isWin = /Windows|Win/i.test(ua);
|
||||
(() => {
|
||||
const tasks = new Map();
|
||||
let ws_conn = null;
|
||||
let cur_page = 1;
|
||||
let is_loading = false;
|
||||
let has_more = true;
|
||||
let total_count = 0;
|
||||
|
||||
function upsert(task) {
|
||||
if (!task || !task.id) return;
|
||||
tasks.set(task.id, {
|
||||
@@ -29,17 +35,24 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
});
|
||||
}
|
||||
function connect(selector) {
|
||||
console.log('[]download connect websocket', FakeAPIServerAddr, WXU.config.remoteServerEnabled);
|
||||
console.log(
|
||||
"[]download connect websocket",
|
||||
FakeAPIServerAddr,
|
||||
WXU.config.remoteServerEnabled,
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
// 在注入场景下始终使用 wss 与 fake 域名通信,由代理层降级/转发到后端
|
||||
const protocol = "wss://";
|
||||
const pathname = FakeAPIServerAddr;
|
||||
const ws = new WebSocket(protocol + pathname + "/ws/channels");
|
||||
ws_conn = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (WXU.downloader) {
|
||||
WXU.downloader.status = "connected";
|
||||
}
|
||||
cur_page = 1;
|
||||
has_more = true;
|
||||
is_loading = false;
|
||||
resolve(true);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
@@ -47,6 +60,7 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
if (WXU.downloader) {
|
||||
WXU.downloader.status = "disconnected";
|
||||
}
|
||||
ws_conn = null;
|
||||
};
|
||||
ws.onerror = (e) => {
|
||||
if (WXU.downloader && WXU.downloader.status !== "connected") {
|
||||
@@ -58,26 +72,44 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
console.log('[]remote ws event', msg);
|
||||
console.log("[]remote ws event", msg);
|
||||
if (msg.type === "tasks") {
|
||||
if (Array.isArray(msg.data)) {
|
||||
msg.data.forEach(upsert);
|
||||
let list = msg.data;
|
||||
if (msg.data && msg.data.list) {
|
||||
list = msg.data.list;
|
||||
total_count = msg.data.total;
|
||||
}
|
||||
__wx_refresh_downloader(selector, tasks);
|
||||
if (Array.isArray(list)) {
|
||||
list.reverse().forEach(upsert);
|
||||
if (list.length < 50) {
|
||||
has_more = false;
|
||||
}
|
||||
} else {
|
||||
has_more = false;
|
||||
}
|
||||
if (is_loading) {
|
||||
is_loading = false;
|
||||
cur_page++;
|
||||
}
|
||||
__wx_refresh_downloader(selector, tasks, total_count);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "clear") {
|
||||
tasks.clear();
|
||||
__wx_refresh_downloader(selector, tasks);
|
||||
total_count = 0;
|
||||
__wx_refresh_downloader(selector, tasks, total_count);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "event") {
|
||||
const evt = msg && msg.data ? msg.data : null;
|
||||
const task = evt ? evt.Task || evt.task : null; // 兼容大小写字段
|
||||
if (task) {
|
||||
if (!tasks.has(task.id)) {
|
||||
total_count++;
|
||||
}
|
||||
upsert(task);
|
||||
}
|
||||
__wx_refresh_downloader(selector, tasks);
|
||||
__wx_refresh_downloader(selector, tasks, total_count);
|
||||
return;
|
||||
}
|
||||
if (msg.type === "api_call") {
|
||||
@@ -86,24 +118,24 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
};
|
||||
if (WXU.config.remoteServerEnabled) {
|
||||
// 额外再连接本地ws用于API调用
|
||||
// const lws = new WebSocket(
|
||||
// protocol + FakeLocalAPIServerAddr + "/ws/channels",
|
||||
// );
|
||||
// lws.onclose = () => {
|
||||
// WXU.error({ msg: "本地ws连接已关闭" });
|
||||
// };
|
||||
// lws.onerror = (e) => {
|
||||
// WXU.error({ msg: "本地ws连接发生错误" });
|
||||
// };
|
||||
// lws.onmessage = (ev) => {
|
||||
// const [err, msg] = WXU.parseJSON(ev.data);
|
||||
// if (err) {
|
||||
// return;
|
||||
// }
|
||||
// if (msg.type === "api_call") {
|
||||
// __wx_handle_api_call(msg.data, lws);
|
||||
// }
|
||||
// };
|
||||
const lws = new WebSocket(
|
||||
protocol + FakeLocalAPIServerAddr + "/ws/channels",
|
||||
);
|
||||
lws.onclose = () => {
|
||||
WXU.error({ msg: "本地ws连接已关闭" });
|
||||
};
|
||||
lws.onerror = (e) => {
|
||||
WXU.error({ msg: "本地ws连接发生错误" });
|
||||
};
|
||||
lws.onmessage = (ev) => {
|
||||
const [err, msg] = WXU.parseJSON(ev.data);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
if (msg.type === "api_call") {
|
||||
__wx_handle_api_call(msg.data, lws);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -125,7 +157,7 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
const t = tasks.get(id);
|
||||
if (t) {
|
||||
tasks.set(id, { ...t, status: "running" });
|
||||
__wx_refresh_downloader("#downloader_container", tasks);
|
||||
__wx_refresh_downloader("#downloader_container", tasks, total_count);
|
||||
}
|
||||
}
|
||||
const $task_action_btn = e.target.closest("[data-action]");
|
||||
@@ -186,18 +218,19 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
}
|
||||
if (action === "delete") {
|
||||
tasks.delete(id);
|
||||
__wx_refresh_downloader("#downloader_container", tasks);
|
||||
if (total_count > 0) total_count--;
|
||||
__wx_refresh_downloader("#downloader_container", tasks, total_count);
|
||||
} else if (action === "pause") {
|
||||
const t = tasks.get(id);
|
||||
if (t) {
|
||||
tasks.set(id, { ...t, status: "paused" });
|
||||
__wx_refresh_downloader("#downloader_container", tasks);
|
||||
__wx_refresh_downloader("#downloader_container", tasks, total_count);
|
||||
}
|
||||
} else if (action === "resume") {
|
||||
const t = tasks.get(id);
|
||||
if (t) {
|
||||
tasks.set(id, { ...t, status: "running" });
|
||||
__wx_refresh_downloader("#downloader_container", tasks);
|
||||
__wx_refresh_downloader("#downloader_container", tasks, total_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +286,7 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
label: "清空记录",
|
||||
onClick: async () => {
|
||||
moredropdown$.hide();
|
||||
|
||||
|
||||
// methods
|
||||
},
|
||||
}),
|
||||
@@ -313,6 +346,34 @@ var isWin = /Windows|Win/i.test(ua);
|
||||
WXU.observe_node(".home-header", () => {
|
||||
insert_downloader();
|
||||
});
|
||||
|
||||
// document.addEventListener(
|
||||
// "scroll",
|
||||
// (e) => {
|
||||
// if (e.target && e.target.id === "downloader_container") {
|
||||
// const el = e.target;
|
||||
// if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
|
||||
// if (
|
||||
// !is_loading &&
|
||||
// has_more &&
|
||||
// ws_conn &&
|
||||
// ws_conn.readyState === WebSocket.OPEN
|
||||
// ) {
|
||||
// is_loading = true;
|
||||
// console.log("[]fetching next page", cur_page + 1);
|
||||
// ws_conn.send(
|
||||
// JSON.stringify({
|
||||
// type: "fetch_tasks",
|
||||
// page: cur_page + 1,
|
||||
// limit: 50,
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// true,
|
||||
// );
|
||||
})();
|
||||
|
||||
async function __wx_handle_api_call(msg, socket) {
|
||||
|
||||
@@ -171,8 +171,9 @@ async function __wx_handle_api_call(msg, socket) {
|
||||
function GopeedDownloaderPanel() {
|
||||
const tasks = [];
|
||||
const tasks_ = ref(tasks);
|
||||
const task_count_ = computed(tasks_, (t) => {
|
||||
return t.length;
|
||||
const total_ = ref(0);
|
||||
const task_count_ = computed(total_, (t) => {
|
||||
return t > 0 ? t : tasks_.value.length;
|
||||
});
|
||||
const runningCount_ = computed(tasks_, (t) => {
|
||||
return t.filter((v) => v.status === "running").length;
|
||||
@@ -235,7 +236,13 @@ function GopeedDownloaderPanel() {
|
||||
return;
|
||||
}
|
||||
if (msg.type === "tasks") {
|
||||
tasks_.value = msg.data;
|
||||
if (Array.isArray(msg.data)) {
|
||||
tasks_.value = msg.data;
|
||||
total_.value = msg.data.length;
|
||||
} else if (msg.data.list) {
|
||||
tasks_.value = msg.data.list;
|
||||
total_.value = msg.data.total;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === "clear") {
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ func (fp *FilenameProcessor) truncateString(s string, maxBytes int) string {
|
||||
|
||||
// 验证和清理文件名
|
||||
func (fp *FilenameProcessor) SanitizeFilename(filename string) (string, error) {
|
||||
fmt.Println("SanitizeFilename", filename)
|
||||
// fmt.Println("SanitizeFilename", filename)
|
||||
if filename == "" {
|
||||
return "", fmt.Errorf("filename cannot be empty")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user