diff --git a/.gitignore b/.gitignore index 4befed3..980b762 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -.DS_Store -.idea +.DS_Store +.idea +*.syso +*.exe diff --git a/README.md b/README.md index 93c1769..fb472de 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ -# KakaoTalkAdBlock - -AdBlocker for KakaoTalk Windows client. (native, alpha) - -## Update History - -- Go [Releases](https://github.com/blurfx/KakaoTalkAdBlock/releases) page - -## At a glance - -![](https://raw.githubusercontent.com/blurfx/KakaoTalkAdBlock/master/kakaotalk.png) - -This program runs in the tray. - -![](https://raw.githubusercontent.com/blurfx/KakaoTalkAdBlock/master/tray.png) +# KakaoTalkAdBlock + +AdBlocker for KakaoTalk Windows client. + +## At a glance + +![](https://raw.githubusercontent.com/blurfx/KakaoTalkAdBlock/main/kakaotalk.png) + +This program runs in the tray. To exit, double-click the tray icon. + +![](https://raw.githubusercontent.com/blurfx/KakaoTalkAdBlock/main/tray.png) diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..87a9ca5 --- /dev/null +++ b/build.bat @@ -0,0 +1,9 @@ +@echo off + +pushd winres + +go-winres simply -icon=icon.ico + +popd + +go build -o KakaoTalkAdBlock.exe -ldflags "-H windowsgui -s -w" .\cmd\main.go diff --git a/cmd/main.go b/cmd/main.go index 0ca4413..c20661b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import "kakaotalkadblock/internal" +import _ "kakaotalkadblock/winres" func main() { internal.Run() diff --git a/internal/ad.go b/internal/ad.go index 74e56a8..369769b 100644 --- a/internal/ad.go +++ b/internal/ad.go @@ -5,7 +5,7 @@ import ( "golang.org/x/sys/windows" - "kakaotalkadblock/internal/win" + "kakaotalkadblock/internal/win/winapi" ) const ( @@ -16,51 +16,51 @@ const ( func HidePopupAd() { var popupHandle windows.HWND for { - popupHandle = win.FindWindowEx(0, popupHandle, "", "") + popupHandle = winapi.FindWindowEx(0, popupHandle, "", "") if popupHandle == 0 { break } - if win.GetParent(popupHandle) != 0 { + if winapi.GetParent(popupHandle) != 0 { continue } - className := win.GetClassName(popupHandle) + className := winapi.GetClassName(popupHandle) if !strings.Contains(className, "RichPopWnd") { continue } - rect := new(win.Rect) - _ = win.GetWindowRect(popupHandle, rect) + rect := new(winapi.Rect) + _ = winapi.GetWindowRect(popupHandle, rect) width := rect.Right - rect.Left height := rect.Bottom - rect.Top if width == 300 && height == 150 { - win.SendMessage(popupHandle, win.WM_CLOSE, 0, 0) + winapi.SendMessage(popupHandle, winapi.WmClose, 0, 0) } } } func HideMainWindowAd(windowClass string, handle windows.HWND) { if windowClass == "BannerAdWnd" { - win.ShowWindow(handle, 0) - win.SetWindowPos(handle, 0, 0, 0, 0, 0, win.SWP_NOMOVE) + winapi.ShowWindow(handle, 0) + winapi.SetWindowPos(handle, 0, 0, 0, 0, 0, winapi.SwpNomove) } } -func HideLockScreenAdArea(windowText string, rect *win.Rect, handle windows.HWND) { +func HideLockScreenAdArea(windowText string, rect *winapi.Rect, handle windows.HWND) { if strings.HasPrefix(windowText, "LockModeView") { width := rect.Right - rect.Left - LayoutShadowPadding height := rect.Bottom - rect.Top - win.UpdateWindow(handle) - win.SetWindowPos(handle, 0, 0, 0, width, height, win.SWP_NOMOVE) + winapi.UpdateWindow(handle) + winapi.SetWindowPos(handle, 0, 0, 0, width, height, winapi.SwpNomove) } } -func HideMainViewAdArea(windowText string, rect *win.Rect, handle windows.HWND) { +func HideMainViewAdArea(windowText string, rect *winapi.Rect, handle windows.HWND) { if strings.HasPrefix(windowText, "OnlineMainView") { width := rect.Right - rect.Left - LayoutShadowPadding height := rect.Bottom - rect.Top - MainViewPadding if height < 1 { return } - win.UpdateWindow(handle) - win.SetWindowPos(handle, 0, 0, 0, width, height, win.SWP_NOMOVE) + winapi.UpdateWindow(handle) + winapi.SetWindowPos(handle, 0, 0, 0, width, height, winapi.SwpNomove) } } diff --git a/internal/app.go b/internal/app.go index 603125f..04c9a5c 100644 --- a/internal/app.go +++ b/internal/app.go @@ -9,6 +9,7 @@ import ( "unsafe" "kakaotalkadblock/internal/win" + "kakaotalkadblock/internal/win/winapi" "golang.org/x/sys/windows" ) @@ -27,15 +28,15 @@ func uint8ToStr(arr []uint8) string { func watch() { const executeable = "kakaotalk.exe" var ( - pe32 win.ProcessEntry32 + pe32 winapi.ProcessEntry32 szExeFile string ) - snapshot := win.CreateToolhelp32Snapshot(win.TH32CS_SNAPPROCESS, 0) + snapshot := winapi.CreateToolhelp32Snapshot(winapi.Th32csSnapprocess, 0) pe32.DwSize = uint32(unsafe.Sizeof(pe32)) var enumWindow = syscall.NewCallback(func(handle windows.HWND, processId uintptr) uintptr { - win.GetWindowThreadProcessId(handle, &pe32.Th32ProcessID) + winapi.GetWindowThreadProcessId(handle, &pe32.Th32ProcessID) if processId == uintptr(pe32.Th32ProcessID) { handles = append(handles, handle) @@ -47,16 +48,16 @@ func watch() { mutex.Lock() handles = handles[:0] - if win.Process32First(uintptr(snapshot), &pe32) { + if winapi.Process32First(uintptr(snapshot), &pe32) { for { szExeFile = uint8ToStr(pe32.SzExeFile[:]) if strings.ToLower(szExeFile) == executeable { - win.EnumWindows(enumWindow, uintptr(pe32.Th32ProcessID)) + winapi.EnumWindows(enumWindow, uintptr(pe32.Th32ProcessID)) break } - if !win.Process32Next(uintptr(snapshot), &pe32) { + if !winapi.Process32Next(uintptr(snapshot), &pe32) { break } } @@ -81,13 +82,13 @@ func removeAd() { } childHandles = childHandles[:0] var handle windows.HWND - win.EnumChildWindows(wnd, enumWindow, uintptr(unsafe.Pointer(&handle))) + winapi.EnumChildWindows(wnd, enumWindow, uintptr(unsafe.Pointer(&handle))) - rect := new(win.Rect) - win.GetWindowRect(wnd, rect) + rect := new(winapi.Rect) + winapi.GetWindowRect(wnd, rect) for _, childHandle := range childHandles { - className := win.GetClassName(childHandle) - windowText := win.GetWindowText(childHandle) + className := winapi.GetClassName(childHandle) + windowText := winapi.GetWindowText(childHandle) HideMainWindowAd(className, childHandle) HideMainViewAdArea(windowText, rect, childHandle) HideLockScreenAdArea(windowText, rect, childHandle) @@ -100,8 +101,15 @@ func removeAd() { } func Run() { + var quit = make(chan struct{}) + trayIcon := win.NewTrayIcon(&quit) + trayIcon.Show() + defer trayIcon.Hide() go watch() go removeAd() - select {} + select { + case <-quit: + return + } } diff --git a/internal/win/tray_icon.go b/internal/win/tray_icon.go new file mode 100644 index 0000000..5a7d460 --- /dev/null +++ b/internal/win/tray_icon.go @@ -0,0 +1,109 @@ +package win + +import ( + "unsafe" + + "golang.org/x/sys/windows" + + "kakaotalkadblock/internal/win/winapi" +) + +var quit *chan struct{} + +func wndProc(hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr { + switch msg { + case winapi.WmTrayicon: + switch uint16(lParam) { + case winapi.WmLbuttondblclk: + close(*quit) + } + case winapi.WmDestroy: + winapi.PostQuitMessage(0) + default: + return winapi.DefWindowProc(hWnd, msg, wParam, lParam) + } + return 0 +} + +func createMainWindow() (uintptr, error) { + hInstance, err := winapi.GetModuleHandle(nil) + if err != nil { + return 0, err + } + + wndClass, _ := windows.UTF16PtrFromString("KakaoTalkAdBlock") + + var windowClass winapi.WindowClassEx + + windowClass.CbSize = uint32(unsafe.Sizeof(windowClass)) + windowClass.LpfnWndProc = windows.NewCallback(wndProc) + windowClass.HInstance = hInstance + windowClass.LpszClassName = wndClass + if _, err := winapi.RegisterClassEx(&windowClass); err != nil { + return 0, err + } + + handle, err := winapi.CreateWindowEx( + 0, + wndClass, + windows.StringToUTF16Ptr("KakaoTalkAdBlock"), + winapi.WsOverlappedwindow, + winapi.CwUsedefault, + winapi.CwUsedefault, + 1, + 1, + 0, + 0, + hInstance, + nil) + if err != nil { + return 0, err + } + + return handle, nil +} + +type TrayIcon struct { + notifyIconData winapi.NotifyIconData +} + +func NewTrayIcon(quitChan *chan struct{}) *TrayIcon { + var data winapi.NotifyIconData + data.CbSize = uint32(unsafe.Sizeof(data)) + data.UFlags = winapi.NifIcon | winapi.NifMessage | winapi.NifInfo + data.UCallbackMessage = winapi.WmTrayicon + + hInst, err := winapi.GetModuleHandle(nil) + if err != nil { + panic(err) + } + icon, err := winapi.LoadIcon(hInst, winapi.MakeIntResource(1)) + if err != nil { + panic(err) + } + data.HIcon = icon + + quit = quitChan + return &TrayIcon{ + notifyIconData: data, + } +} + +func (t *TrayIcon) Show() { + if t.notifyIconData.HWnd == 0 { + handle, err := createMainWindow() + if err != nil { + panic(err) + } + t.notifyIconData.HWnd = handle + } + if err := winapi.ShellNotifyIcon(winapi.NimAdd, &t.notifyIconData); err != nil { + panic(err) + } +} + +func (t *TrayIcon) Hide() { + if err := winapi.ShellNotifyIcon(winapi.NimDelete, &t.notifyIconData); err != nil { + panic(err) + } +} diff --git a/internal/win/winapi/constant.go b/internal/win/winapi/constant.go new file mode 100644 index 0000000..58ae87d --- /dev/null +++ b/internal/win/winapi/constant.go @@ -0,0 +1,31 @@ +package winapi + +const ( + Th32csSnapprocess = 0x2 + MaxPath = 260 + + NimAdd = 0x00000000 + NimDelete = 0x00000002 + + NifMessage = 0x00000001 + NifIcon = 0x00000002 + NifInfo = 0x00000010 + + CwUsedefault = ^0x7fffffff + + WsCaption = 0x00c00000 + WsMaximizebox = 0x00010000 + WsMinimizebox = 0x00020000 + WsOverlapped = 0x00000000 + WsSysmenu = 0x00080000 + WsThickframe = 0x00040000 + WsOverlappedwindow = WsOverlapped | WsCaption | WsSysmenu | WsThickframe | WsMinimizebox | WsMaximizebox + + WmDestroy = 0x0002 + WmClose = 0x10 + WmLbuttondblclk = 0x0203 + WmApp = 0x8000 + WmTrayicon = WmApp + 1 + + SwpNomove = 0x0002 +) diff --git a/internal/win/kernel32.go b/internal/win/winapi/kernel32.go similarity index 53% rename from internal/win/kernel32.go rename to internal/win/winapi/kernel32.go index 8649ce0..ae20f58 100644 --- a/internal/win/kernel32.go +++ b/internal/win/winapi/kernel32.go @@ -1,4 +1,4 @@ -package win +package winapi import ( "unsafe" @@ -6,10 +6,20 @@ import ( "golang.org/x/sys/windows" ) -const ( - TH32CS_SNAPPROCESS = 0x2 - MAX_PATH = 260 -) +type WindowClassEx struct { + CbSize uint32 + Style uint32 + LpfnWndProc uintptr + CbClsExtra int32 + CbWndExtra int32 + HInstance uintptr + HIcon uintptr + HCursor uintptr + HbrBackground uintptr + LpszMenuName *uint16 + LpszClassName *uint16 + HIconSm uintptr +} type ProcessEntry32 struct { DwSize uint32 @@ -21,14 +31,15 @@ type ProcessEntry32 struct { Th32ParentProcessID uint32 PcPriClassBase uint32 DwFlags uint32 - SzExeFile [MAX_PATH]uint8 + SzExeFile [MaxPath]uint8 } var ( kernel32 = windows.NewLazySystemDLL("kernel32.dll") createToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot") - procProcess32First = kernel32.NewProc("Process32First") - procProcess32Next = kernel32.NewProc("Process32Next") + process32First = kernel32.NewProc("Process32First") + process32Next = kernel32.NewProc("Process32Next") + getModuleHandle = kernel32.NewProc("GetModuleHandleW") ) func CreateToolhelp32Snapshot(flags uint32, pid uint32) windows.HWND { @@ -37,7 +48,7 @@ func CreateToolhelp32Snapshot(flags uint32, pid uint32) windows.HWND { } func Process32First(hSnapshot uintptr, pe *ProcessEntry32) bool { - ret, _, _ := procProcess32First.Call( + ret, _, _ := process32First.Call( hSnapshot, uintptr(unsafe.Pointer(pe)), ) @@ -45,9 +56,17 @@ func Process32First(hSnapshot uintptr, pe *ProcessEntry32) bool { } func Process32Next(hSnapshot uintptr, pe *ProcessEntry32) bool { - ret, _, _ := procProcess32Next.Call( + ret, _, _ := process32Next.Call( hSnapshot, uintptr(unsafe.Pointer(pe)), ) return ret != 0 } + +func GetModuleHandle(lpModuleName *uint16) (uintptr, error) { + r, _, err := getModuleHandle.Call(uintptr(unsafe.Pointer(lpModuleName))) + if r == 0 { + return 0, err + } + return r, nil +} diff --git a/internal/win/winapi/shell32.go b/internal/win/winapi/shell32.go new file mode 100644 index 0000000..0c9090e --- /dev/null +++ b/internal/win/winapi/shell32.go @@ -0,0 +1,49 @@ +package winapi + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +type NotifyIconData struct { + CbSize uint32 + HWnd uintptr + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon uintptr + SzTip [128]uint16 + DwState uint32 + DwStateMask uint32 + SzInfo [256]uint16 + UVersion uint32 + SzInfoTitle [64]uint16 + DwInfoFlags uint32 + GuidItem GUID + HBalloonIcon uintptr +} + +var ( + shell32 = windows.NewLazySystemDLL("shell32.dll") + shellNotifyIcon = shell32.NewProc("Shell_NotifyIconW") +) + +func MakeIntResource(i uint16) *uint16 { + return (*uint16)(unsafe.Pointer(uintptr(i))) +} + +func ShellNotifyIcon(dwMessage uintptr, notifyIconData *NotifyIconData) error { + r, _, err := shellNotifyIcon.Call(dwMessage, uintptr(unsafe.Pointer(notifyIconData))) + if r == 0 { + return err + } + return nil +} diff --git a/internal/win/user32.go b/internal/win/winapi/user32.go similarity index 50% rename from internal/win/user32.go rename to internal/win/winapi/user32.go index c30976d..4d4145d 100644 --- a/internal/win/user32.go +++ b/internal/win/winapi/user32.go @@ -1,4 +1,4 @@ -package win +package winapi import ( "syscall" @@ -14,25 +14,25 @@ type Rect struct { Bottom int32 } -const ( - SWP_NOMOVE = 0x0002 - WM_CLOSE = 0x10 -) - var ( - libuser32 = windows.NewLazySystemDLL("user32.dll") - getClassName = libuser32.NewProc("GetClassNameW") - enumChildWindows = libuser32.NewProc("EnumChildWindows") - enumWindows = libuser32.NewProc("EnumWindows") - showWindow = libuser32.NewProc("ShowWindow") - findWindowEx = libuser32.NewProc("FindWindowExW") - getParent = libuser32.NewProc("GetParent") - setWindowPos = libuser32.NewProc("SetWindowPos") - getWindowText = libuser32.NewProc("GetWindowTextW") - getWindowRect = libuser32.NewProc("GetWindowRect") - updateWindow = libuser32.NewProc("UpdateWindow") - sendMessage = libuser32.NewProc("SendMessageW") - getWindowThreadProcessId = libuser32.NewProc("GetWindowThreadProcessId") + user32 = windows.NewLazySystemDLL("user32.dll") + getClassName = user32.NewProc("GetClassNameW") + enumChildWindows = user32.NewProc("EnumChildWindows") + enumWindows = user32.NewProc("EnumWindows") + showWindow = user32.NewProc("ShowWindow") + findWindowEx = user32.NewProc("FindWindowExW") + getParent = user32.NewProc("GetParent") + setWindowPos = user32.NewProc("SetWindowPos") + getWindowText = user32.NewProc("GetWindowTextW") + getWindowRect = user32.NewProc("GetWindowRect") + updateWindow = user32.NewProc("UpdateWindow") + sendMessage = user32.NewProc("SendMessageW") + getWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") + loadIcon = user32.NewProc("LoadIconW") + postQuitMessage = user32.NewProc("PostQuitMessage") + defWindowProc = user32.NewProc("DefWindowProcW") + registerClassEx = user32.NewProc("RegisterClassExW") + createWindowEx = user32.NewProc("CreateWindowExW") ) func cStr(str string) uintptr { @@ -103,3 +103,67 @@ func GetWindowThreadProcessId(hWnd windows.HWND, dwProcessId *uint32) uint32 { r, _, _ := getWindowThreadProcessId.Call(uintptr(hWnd), uintptr(unsafe.Pointer(dwProcessId))) return uint32(r) } + +func PostQuitMessage(nExitCode int32) { + r, _, err := postQuitMessage.Call(uintptr(nExitCode)) + if r == 0 { + panic(err) + } +} + +func DefWindowProc( + hWnd uintptr, + Msg uint32, + wParam, lParam uintptr) uintptr { + r, _, _ := defWindowProc.Call( + hWnd, + uintptr(Msg), + wParam, + lParam) + return r +} + +func RegisterClassEx(Arg1 *WindowClassEx) (uint16, error) { + r, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(Arg1))) + if r == 0 { + return 0, err + } + return uint16(r), nil +} + +func CreateWindowEx( + dwExStyle uint32, + lpClassName, lpWindowName *uint16, + dwStyle uint32, + X, Y, nWidth, nHeight int32, + hWndParent, hMenu, hInstance uintptr, + lpParam unsafe.Pointer) (uintptr, error) { + r, _, err := createWindowEx.Call( + uintptr(dwExStyle), + uintptr(unsafe.Pointer(lpClassName)), + uintptr(unsafe.Pointer(lpWindowName)), + uintptr(dwStyle), + uintptr(X), + uintptr(Y), + uintptr(nWidth), + uintptr(nHeight), + hWndParent, + hMenu, + hInstance, + uintptr(lpParam)) + if r == 0 { + return 0, err + } + return r, nil +} + +func LoadIcon(instance uintptr, iconName *uint16) (uintptr, error) { + ret, _, err := loadIcon.Call( + instance, + uintptr(unsafe.Pointer(iconName)), + ) + if ret == 0 { + return 0, err + } + return ret, nil +} diff --git a/tray.png b/tray.png new file mode 100644 index 0000000..b23c30a Binary files /dev/null and b/tray.png differ diff --git a/winres/icon.ico b/winres/icon.ico new file mode 100644 index 0000000..0c291d6 Binary files /dev/null and b/winres/icon.ico differ diff --git a/winres/winres.go b/winres/winres.go new file mode 100644 index 0000000..475afc3 --- /dev/null +++ b/winres/winres.go @@ -0,0 +1 @@ +package winres